SpringSecurity集成第三方登录

SpringSecurity 集成第三方登录

认证及自定义流程

image-20240403104027920

首先我们提供一个实现了AbstractAuthenticationProcessingFilter抽象类的过滤器,用来代替UsernamePasswordAuthenticationFilter逻辑,然后提供一个AuthenticationProvider实现类代替AbstractUserDetailsAuthenticationProvider或DaoAuthenticationProvider,最后再提供一个UserDetailsService实现类。

1.验证码登录

1.通用过滤器实现–ThirdAuthenticationFilter

这个ThirdAuthenticationFilter过滤器我们可以仿照UsernamePasswordAuthenticationFilter来实现(也实现了AbstractAuthenticationProcessingFilter抽象类),主要是重新定义了attemptAuthentication()方法,这里需要根据“authType”参数值的类别构建不同的AbstractAuthenticationToken,具体实现如下:

    //验证类型,比如Sms,uernamepassword等private String authTypeParameter = "authType";//对应用户名或手机号等private String principalParameter = "principal";//对应密码或验证码等private String credentialsParameter = "credentials";private boolean postOnly = true;public ThirdAuthenticationFilter() {super(new AntPathRequestMatcher("/login/doLogin", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String authType = request.getParameter(authTypeParameter);if(StringUtils.isEmpty(authType)){authType = AuthTypeEnum.AUTH_TYPE_DEFAULT.getAuthType();}String principal = request.getParameter(principalParameter);String credentials = request.getParameter(credentialsParameter);AbstractAuthenticationToken authRequest = null;switch (authType){case "sms":authRequest = new SmsAuthenticationToken(principal, credentials);((SmsAuthenticationToken)authRequest).setCode((String)request.getSession().getAttribute("code"));break;case "github":authRequest = new GithubAuthenticationToken(principal, credentials);break;case "default":authRequest = new UsernamePasswordAuthenticationToken(principal, credentials);}authRequest.setDetails(authenticationDetailsSource.buildDetails(request));return this.getAuthenticationManager().authenticate(authRequest);}
}

定义了ThirdAuthenticationSecurityConfig 配置类,我们还需要在SpringSecurity配置类中应用才能生效,具体实现如下:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/error","/login/**","/login/goLogin","/login/doLogin","/login/code","/login/authorization_code").anonymous().anyRequest().authenticated().and().formLogin().loginPage("/login/goLogin").loginProcessingUrl("/login/doLogin").failureUrl("/login/error").permitAll().successHandler(new QriverAuthenticationSuccessHandler("/index/toIndex"));//这里我们省略了一些配置 ……//应用前面定义的配置http.apply(thirdAuthenticationSecurityConfig);
}

至此,我们定义的通用第三方过滤器就完成了,并且也完成了在SpringSecurity中生效的配置。下面我们就开始分别实现不同类型登录的具体过程。

在ThirdAuthenticationFilter 类的attemptAuthentication()方法中,我们通过authType类型,然后创建对应的Authentication实现来实现不同方式的登录,这里我们主要实现了如下三种方式,我们分别梳理一下。

三、默认的登录过程
  默认的登录过程,即根据用户名密码进行登录,需要使用到UsernamePasswordAuthenticationToken,当“authType”参数为"default"时,这里就会创建UsernamePasswordAuthenticationToken对象,然后后续通过ProviderManager的authenticate()方法,最后就会调用AbstractUserDetailsAuthenticationProvider(DaoAuthenticationProvider)的 authenticate()方法,最终又会调用定义的UserDetailsService实现类。这是默认的过程,这里就不再重复其中的逻辑,除了UserDetailsService实现类需要自己定义,其他都是SpringSecurity提供的实现类。

四、短信验证码登录实现
  短信验证码登录,是最贴近用户名密码登录的一种方式,所以我们完全可以仿照用户名密码这种方式实现。我们这里先梳理一下短信验证码登录的业务逻辑:首先,登录界面输入手机号码,然后再点击“获取验证码”按钮获取短信验证码,然后输入收到的短信验证码,最后点击“登录”按钮进行登录认证。和用户名密码登录相比,短信验证码登录多了一个获取验证码的过程,其他其实都是一样的,我们下面逐步实现短信验证码登录:

@RestController
@RequestMapping("/login")
public class SmsValidateCodeController {//生成验证码的实例对象@Autowiredprivate ValidateCodeGenerator smsCodeGenerator;//调用服务商接口,发送短信验证码的实例对象@Autowiredprivate DefaultSmsCodeSender defaultSmsCodeSender;@RequestMapping("/code")public String createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));String mobile = (String)request.getParameter("principal");request.getSession().setAttribute("code",smsCode.getCode());defaultSmsCodeSender.send(mobile, smsCode.getCode());System.out.println("验证码:" + smsCode.getCode());return "验证码发送成功!";}
}

在上述方法中,我们注入了smsCodeGenerator和defaultSmsCodeSender两个实例对象,分别用来生成验证码和发送短信验证码,这个可以根据项目的实际情况进行定义和实现,这里不再贴出其中的实现。同时在createSmsCode()方法中,还有一点需要注意的就是,我们发出去的短信验证码需要进行保存,方便后续登录时进行验证,这个也可以选择很多方法,比如说会话、数据库、缓存等,我这里为了简单,直接存到了session会话中了。

然后,我们前面定义ThirdAuthenticationFilter过滤器时,根据登录方式不同,需要对应的Authentication对象,这里我们还需要创建短信验证登录需要的Authentication类,这里我们可以仿照UsernamePasswordAuthenticationToken类进行编写,实现如下

public class SmsAuthenticationToken  extends AbstractAuthenticationToken {//对应手机号码private final Object principal;//对应手机验证码private Object credentials;//后台存储的短信验证码,用于验证前端传过来的是否正确private String code;public SmsAuthenticationToken(String mobile, Object credentials){super(null);this.principal = mobile;this.credentials = credentials;this.code = code;setAuthenticated(false);}public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object credentials){super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}public String getCode() {return code;}public void setCode(String code) {this.code = code;}@Overridepublic void eraseCredentials() {super.eraseCredentials();credentials = null;}
}

在SmsAuthenticationToken 类中,我们增加了一个code属性,其实该属性不是必须的,我这里是为了方便传递存储在session会话中的验证码而添加的,如果使用缓存或数据库进行存储验证码,该属性就可以省略。

在AuthenticationManager的authenticate()方法中,会根据Authentication类型选择AuthenticationProvider对象,所以我们这里自定义短信验证码需要的AuthenticationProvider对象,实现如下:

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider{@Autowired@Qualifier("smsUserDetailsService")private UserDetailsService userDetailsService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;String mobile = (String)token.getPrincipal();//首先,验证验证码是否正确String code = (String)token.getCredentials();String sCode = token.getCode();if(StringUtils.isEmpty(code) || !code.equalsIgnoreCase(sCode)){throw new BadCredentialsException("手机验证码错误(Bad credentials),请重试!");}//然后,查询对应用户UserDetails user = userDetailsService.loadUserByUsername(mobile);if (Objects.isNull(user)) {throw new InternalAuthenticationServiceException("根据手机号:" + mobile + ",无法获取对应的用户信息!");}SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(user.getUsername(), user.getAuthorities(), token.getCredentials());authenticationResult.setDetails(token.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> authentication) {return SmsAuthenticationToken.class.isAssignableFrom(authentication);}
}

在SmsAuthenticationProvider 中,supports()方法决定了该实例对象仅支持SmsAuthenticationToken对象的验证。同时,根据authenticate()方法传递参数authentication对象(包括了登录信息:手机号和验证码,session存储的验证码),我们这里session存储的验证码,是因为我们采用了会话存储的方式,如果使用数据库,我们这里就可以通过手机号,去数据库或缓存查询对应的验证码,然后和authentication对象传递过来的验证码进行比对,验证成功,说明登录认证成功,否则登录认证失败。登录成功后,我们就可以调用userDetailsService对象的loadUserByUsername()方法获取登录用户的其他相关信息(权限等),具体实现在自定义的SmsUserDetailsService类中实现,具体如下:

@Component("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {private Logger logger = LoggerFactory.getLogger(SmsUserDetailsService.class);@Autowiredprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//1、查询用户信息SysUser user = new SysUser();user.setMobile(username);SysUser qUser = sysUserService.getOne(new QueryWrapper<>(user),true);if(qUser == null) {logger.info("手机号为”" + username + "“的用户不存在!!!");throw new UsernameNotFoundException("手机号为”" + username + "“的用户不存在!!!");}//2、封装用户角色UserRole userRole = sysUserService.getRoleByUserId(qUser.getId());Collection<GrantedAuthority> authorities = new ArrayList<>();authorities.add(new SimpleGrantedAuthority(String.valueOf(userRole.getRoleId())));return new LoginUser(qUser.getUsername(), qUser.getPassword(),authorities);} 
}

2.GitHub登录

和短信验证码登录认证相比,Github登录又会有自己的特殊性,我们这里先梳理一下基于Github进行登录验证的大致逻辑:首先,点击Github登录认证按钮,然后会跳转到github登录界面,输入github系统的用户名密码,登录成功,就会跳转到我们自己的系统中的首页。和基于用户名密码的登录方式相比,Github登录不需要类似用户名和密码这样的输入(在自己的系统中),同时又需要根据获取到的github用户信息,换取在自己系统对应的用户信息。具体实现步骤如下:

在github的配置省略

@Controller
@RequestMapping("/login")
public class GithubValidateController {@Autowiredprivate GithubClientService githubClientService;@RequestMapping("/authorization_code")public void authorization_code(HttpServletRequest request, HttpServletResponse response, String code) throws ServletRequestBindingException, IOException {//github登录验证,并获取access_tokenMap<String,String> resp = githubClientService.queryAccessToken(code);//跳转本系统的登录流程,获取用户信息,实现两个系统用户的对接String url = "http://localhost:8888/qriver-admin/login/doLogin";this.sendByPost(response, url,resp.get("access_token"),"github");//this.sendByPost(response, url,"access_token","github");}public void sendByPost(HttpServletResponse response,String url, String principal, String authType) throws IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");out.println("<HTML>");out.println(" <HEAD><TITLE>Post 方法</TITLE></HEAD>");out.println(" <BODY>");out.println("<form name=\"submitForm\" action=\"" + url + "\" method=\"post\">");out.println("<input type=\"hidden\" name=\"principal\" value=\"" + principal + "\"/>");out.println("<input type=\"hidden\" name=\"authType\" value=\"" + authType + "\"/>");out.println("</from>");out.println("<script>window.document.submitForm.submit();</script> ");out.println(" </BODY>");out.println("</HTML>");out.flush();out.close();}}

“/login/authorization_code”接口对应了我们在Github中配置的回调函数,即在Github登录验证成功后,就会回调该接口,我们就是就在回调方法中,模拟了用户名密码登录的方式,调用了SpringSecurity登录认证需要的“/login/doLogin”接口。这里,我们通过queryAccessToken()方法根据回调传递的code获取对应的accessToken,然后把accessToken作为登录使用的principal 参数值,之而立不需要传递密码,因为我们经过Github授权,就可以认为完成了登录认证的判断过程了。

其中GithubClientService类,提供了获取accessToken和用户信息的两个方法,具体实现方式如下:

@Service
public class GithubClientService {//前面在github中配置时产生的private String clientId = "######";private String clientSecret = "######";private String state = "123";private String redirectUri = "http://localhost:8888/qriver-admin/login/authorization_code";@Autowiredprivate RestTemplate restTemplate;@Nullableprivate WebApplicationContext webApplicationContext;//获取accessTokenpublic Map<String, String> queryAccessToken(String code ){Map<String, String> map = new HashMap<>();map.put("client_id", clientId);map.put("client_secret", clientSecret);map.put("state", state);map.put("code", code);map.put("redirect_uri", redirectUri);Map<String,String> resp = restTemplate.postForObject("https://github.com/login/oauth/access_token", map, Map.class);return resp;}//获取用户信息public Map<String, Object> queryUser(String accessToken){HttpHeaders httpheaders = new HttpHeaders();httpheaders.add("Authorization", "token " + accessToken);HttpEntity<?> httpEntity = new HttpEntity<>(httpheaders);ResponseEntity<Map> exchange = restTemplate.exchange("https://api.github.com/user", HttpMethod.GET, httpEntity, Map.class);System.out.println("exchange.getBody() = " + exchange.getBody());return exchange == null ? null : exchange.getBody();}
}

其实,完成了上述的配置和方式后,后续的方式就和短信验证码的逻辑一样了,这里我们简要的再梳理一下。

首先,我们也需要定义一个基于Github登录需要的Authentication实现类,具体实现和前面的SmsAuthenticationToken类似,这里不再重复贴代码了。

然后,我们再定义一个AuthenticationProvider实现类GithubAuthenticationProvider,具体实现如下:

@Component
public class GithubAuthenticationProvider implements AuthenticationProvider{@Autowired@Qualifier("githubUserDetailsService")private UserDetailsService userDetailsService;@Autowiredprivate GithubClientService githubClientService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {GithubAuthenticationToken token = (GithubAuthenticationToken) authentication;String accessToken = (String)token.getPrincipal();//根据accessToken 获取github用户信息Map<String, Object> userInfo = githubClientService.queryUser(accessToken);//然后,根据github用户,查询对应系统用户信息UserDetails user = userDetailsService.loadUserByUsername((String)userInfo.get("login"));if (Objects.isNull(user)) {throw new InternalAuthenticationServiceException("根据accessToken:" + accessToken + ",无法获取对应的用户信息!");}GithubAuthenticationToken authenticationResult = new GithubAuthenticationToken(user.getUsername(), user.getAuthorities(), token.getCredentials());authenticationResult.setDetails(token.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> authentication) {return GithubAuthenticationToken.class.isAssignableFrom(authentication);}
}

在GithubAuthenticationProvider 类的authenticate()方法中,参数authentication中对应的是Github授权后传递的accessToken值,我们这里需要根据accessToken值换取Github用户信息,这里通过queryUser()方法实现,然后根据github用户名去获取对应的系统用户信息。如果根据github用户名用户获取的系统用户为空,我们可以根据自己的需求,自动生成一个用户或者跳转到注册页面,让用户注册一个页面,这里为了简单,我们直接抛出了一个异常。

关于自定义UserDetailsService实现类,主要需要实现根据github用户名查询对应系统用户的功能

当认证完成后要返回token可以实现AuthenticationSuccessHandler

import org.springframework.security.core.Authentication;  
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {  private final JwtTokenProvider jwtTokenProvider; // 假设你有一个JwtTokenProvider类来生成JWT  public CustomAuthenticationSuccessHandler(JwtTokenProvider jwtTokenProvider) {  this.jwtTokenProvider = jwtTokenProvider;  }  @Override  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {  // 生成JWT  String token = jwtTokenProvider.generateToken(authentication);  // 将JWT添加到响应头中  response.setHeader("Authorization", "Bearer " + token);  // 或者将JWT添加到响应体中(取决于你的API设计)  // response.getWriter().write(token);  response.setStatus(HttpServletResponse.SC_OK);  }  
}

并在securityconfig中设置

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/835932.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

合专家模型 (MoE) 详解

本文转载自&#xff1a;混合专家模型 (MoE) 详解 https://huggingface.co/blog/zh/moe 英文版&#xff1a;https://huggingface.co/blog/moe 文章目录 一、简短总结二、什么是混合专家模型&#xff1f;三、混合专家模型简史四、什么是稀疏性?五、混合专家模型中令牌的负载均衡…

solidworks的进阶操作

目录 1 可以找别人的图 2 渲染 2.1 基本流程 2.2 相机和光源 3 装配图缩放 3.1 将装配图转换为零件 3.2 删除一些细节(可选) 3.3 缩放 4 3dmax文件转换为STL并对STL上色 5 文件是未来版本 1 可以找别人的图 有时需要出一些示意图&#xff0c;像是电脑桌子…

Wix打包后安装包直接签名安装失败原因

生成的游戏启动器wix安装包直接打包后进行签名安装会失败&#xff0c;看安装日志显示的错误为 Failed to extract all files from container, erf: 1:2:0 网上搜到的解决方案 需要用insignia工具解包&#xff0c;解包后的文件签一次名&#xff0c;再打一次包&#xff0c;再…

校园管理系统,基于 SpringBoot+Vue+MySQL 开发的前后端分离的校园管理系统设计实现

目录 一. 前言 二. 功能模块 2.1. 管理员功能模块 2.2. 用户功能模块 2.3. 院校管理员功能模块 三. 部分代码实现 四. 源码下载 一. 前言 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身…

AR人像滤镜SDK解决方案,专业调色,打造个性化风格

视觉内容已成为企业传达品牌价值和吸引用户眼球的重要载体&#xff0c;为满足企业对于高质量、多样化视觉内容的迫切需求&#xff0c;美摄科技凭借先进的AR技术和深厚的图像处理经验&#xff0c;推出了业界领先的AR人像滤镜SDK解决方案。 一、一站式解决方案&#xff0c;覆盖多…

Leetcode3138. 同位字符串连接的最小长度

Every day a Leetcode 题目来源&#xff1a;3138. 同位字符串连接的最小长度 解法1&#xff1a;枚举同位子串的长度 从小到大枚举字符串 t 的长度 len。 因为字符串 s 由字符串 t 和它的同位字符串连接而成&#xff0c;所以 n % len 0。 然后比较所有首字母下标为 0、len…

阿里云域名备案流程

阿里云域名备案流程大致可以分为以下几个步骤&#xff0c;这些信息综合了不同来源的最新流程说明&#xff0c;确保了流程的时效性和准确性&#xff1a; UP贴心的附带了链接&#xff1a; 首次备案流程&#xff1a;ICP首次备案_备案(ICP Filing)-阿里云帮助中心 (aliyun.com) …

政安晨:【Keras机器学习示例演绎】(四十三)—— 使用 KerasNLP 实现英语到西班牙语的翻译

目录 简介 设置 下载数据 解析数据 数据标记化 格式化数据集 建立模型 训练我们的模型 解码测试句子&#xff08;定性分析&#xff09; 解码测试句子&#xff08;定性分析&#xff09; 评估我们的模型&#xff08;定量分析&#xff09; 10 个轮次后&#xff0c;得分…

事务-MYSQL

目录 1.事务操作演示 2.事务四大特性ACID 3.并发事务问题 4. 并发事务演示及隔离级别​编辑​编辑​编辑​编辑​编辑​编辑​编辑 1.事务操作演示 默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。 方式二 2.事务四大特性ACID 原子…

多线程-线程安全

目录 线程安全问题 加锁(synchronized) synchronized 使用方法 synchronized的其他使用方法 synchronized 重要特性(可重入的) 死锁的问题 对 2> 提出问题 对 3> 提出问题 解决死锁 对 2> 进行解答 对4> 进行解答 volatile 关键字 wait 和 notify (重要…

LeetCode例题讲解:844.比较含退格的字符串

给定 s 和 t 两个字符串&#xff0c;当它们分别被输入到空白的文本编辑器后&#xff0c;如果两者相等&#xff0c;返回 true 。# 代表退格字符。 注意&#xff1a;如果对空文本输入退格字符&#xff0c;文本继续为空。 示例 1&#xff1a; 输入&#xff1a;s "ab#c&qu…

llm.c的Makefile

源码 CC ? clang CFLAGS -Ofast -Wno-unused-result -Wno-ignored-pragmas -Wno-unknown-attributes LDFLAGS LDLIBS -lm INCLUDES CFLAGS_COND -marchnative# Find nvcc SHELL_UNAME $(shell uname) REMOVE_FILES rm -f OUTPUT_FILE -o $ CUDA_OUTPUT_FILE -o $# N…

springboot项目打包部署

springboot打包的前提条件jdk必须17以后不然本地运行不来&#xff08;我用的jdk是22&#xff09; 查看自己电脑jdk版本可以参考&#xff08;完美解决Windows10下-更换JDK环境变量后&#xff0c;在cmd下执行仍java -version然出现原来版本的JDK的问题-CSDN博客&#xff09; 1、…

六级翻译笔记

理解加表达 除了专有名词不能自己理解翻译&#xff0c;其它都可以 时态一般唯一 题目里出现有翻译为 客观存在&#xff1a; there be 单词结尾加er和ee的区别&#xff1a;er是主动&#xff0c;ee是被动 中文句子没有被动&#xff0c;也可以英文翻译为被动 中文的状语可以不是…

【无标获取S4与ECC的具体差异的方法题】

首先我们需要对ECC vs S4的差异这个课题要有一个深刻的理解&#xff0c;这不是一个简单并能准确说清楚的课题。 我们需要结合实际项目的具体情况去回答这个问题&#xff0c;因为这个问题本身是没有标准答案的。 首先要了解SAP本身ERP产品线的发展概况&#xff0c;其次我们要…

GitHub操作

远程库-GitHub GitHub网址 GitHub是全球最大的远程库 1. 创建远程库 2. 远程仓库操作 2.1 创建远程仓库别名 git remote -v 查看当前所有远程库地址别名 git remote add 别名 远程地址 设置远程库地址别名 案例操作 起一个别名会出现两个别名&#xff0c;是因为既可以拉取…

C语言 | Leetcode C语言题解之第84题柱状图中最大的矩形

题目&#xff1a; 题解&#xff1a; int largestRectangleArea(int* heights, int heightsSize) {int st[heightsSize];int p[2];p[0]-1,p[1]heightsSize;int size0,result0;st[size]0;for(int i1;i<heightsSize;i){ while(size!0&&heights[i]<heights[st[size-1…

动态规划解决回文子串问题

前言&#xff1a; 回文串相关问题在我们的算法题中算是老生常谈&#xff0c;本文主要介绍如何使用动态规划的思路去解决回文串系列问题。 总体思路&#xff1a; 能够将所有的子串是否是回文的信息&#xff0c;存储在二维dp表中。有了这个dp表&#xff0c;就可以将hard难度转…

【实战】采用jenkins pipeline实现自动构建并部署至k8s

文章目录 前言部署jenkins编写docker-compose-jenkins.yaml配置maven源启动jenkins解锁jenkins Jenkins默认插件及git、镜像仓库、k8s凭证配置host key verification configuration修改为不验证Gitee ssh阿里云镜像仓库ssh编写pipeline安装以下常用插件将kubectl命令文件拷贝到…

E - Yet Another Sigma Problem(ABC字典树)

思路&#xff1a;我们可以发现两个字符串的最长公共前缀就是字典树中的最近公共祖先。然而这道题&#xff0c;比如说某个结点是x个字符串的前缀&#xff0c;那么当前结点对答案的贡献为x * (x - 1) / 2&#xff0c;就是x中任选两个字符串组合&#xff0c;因为在这之前&#xff…