【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索

作者:后端小肥肠

创作不易,未经允许严禁转载。

姊妹篇:

【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客

1. 前言

欢迎来到【Spring Security系列】!在当今数字化时代,安全是任何应用程序都必须优先考虑的核心问题之一。而在众多安全框架中,Spring Security 作为一个功能强大且广泛应用的安全框架,为 Java 应用程序提供了全面的身份验证、授权、攻击防护等功能。而随着移动应用的普及,小程序作为一种轻量级、跨平台的应用形式,其安全性也成为了开发者们关注的焦点。本文将带领您深入探索如何使用 Spring Security 来保护小程序的登录认证,旨在为您提供全方位的学习体验和实践指导。

2. 小程序登录涉及SpringSecurity核心组件介绍

如果要在SpringSecurity默认的用户名密码模式登录模式上扩展小程序登录,涉及到的核心组件如下:

  1. AuthenticationProvider

    创建自定义的AuthenticationProvider,负责处理从微信开放平台获取的用户信息,并进行身份验证。
  2. UserDetailsService

    调整UserDetailsService来获取并管理基于微信OpenID的用户信息。
  3. AuthenticationManager

    确保您的自定义AuthenticationProvider被正确注册到AuthenticationManager中,以便处理小程序登录请求。
  4. SecurityConfigurer

    创建一个SecurityConfigurer来配置Spring Security以支持小程序登录,并将其添加到Spring Security的配置类中。
  5. Filter

    创建一个自定义的过滤器来拦截和处理小程序登录请求,提取微信登录凭证,并将其传递给您的自定义AuthenticationProvider进行处理。

要扩展Spring Security以支持小程序登录,您需要创建自定义的AuthenticationProvider并调整UserDetailsService以处理微信OpenID的用户信息。

3. SpringSecurity集成小程序登录原理

3.1. 小程序登录流程

以下是微信官方文档中小程序登录的流程:

由上图可看出,小程序登录使用微信提供的登录凭证 code,通过微信开放平台的接口获取用户的唯一标识 OpenID 和会话密钥 SessionKey。在集成小程序登录时,我们需要将这些凭证传递给后端服务器,由后端服务器进行校验和处理,最终完成用户的登录认证。

3.2. SpringSecurity集成小程序登录流程梳理

结合SpringSecurity原理,在SpringSecurity中集成小程序登录的流程如下:

  • 小程序端:通过微信登录接口获取登录凭证 code,这里取名为loginCode。
  • 小程序端,通过手机号快速验证组件获取phoneCode。
  • 小程序端,获取用户昵称(nickName)和用户头像(imageUrl)地址。
  • 小程序端:将登录凭证 loginCodephoneCodenickNameimageUrl发送给后端服务器。
  • 后端服务器:接收到登录凭证 loginCode后,调用微信开放平台的接口,换取用户的唯一标识 OpenID 和会话密钥 SessionKey
  • 后端服务器:根据 OpenID 查询用户信息,如果用户不存在,则创建新用户;如果用户已存在,则返回用户信息。
  • 后端服务器:生成用户的身份认证信息JWT Token,返回给小程序端。
  • 小程序端:存储用户的身份认证信息,后续请求携带该信息进行访问控制。

大体流程只是在3.1小程序登录流程上做了细化,图我就不画了(因为懒)。

3.3. 小程序登录接口设计

小程序登录接口如下图所示:

由上图所示,我们需要传入loginCode,phoneCode(获取手机号),nickName(昵称用于登录后展示),imageUrl(头像用于登录后展示)这几个必传参数。

4. 核心代码讲解

4.1. 小程序端获取必要参数

1. 小程序端调用微信登录接口,获取用户登录凭证 loginCode。

wx.login({success (res) {if (res.code) {//发起网络请求wx.request({url: 'https://example.com/onLogin',data: {code: res.code}})} else {console.log('登录失败!' + res.errMsg)}}
})

2. 获取PhoneCode

3. 获取头像昵称

4.2. 编写WeChatAuthenticationFilter

public class WeChatAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private final String loginCode = "loginCode";private final String phoneCode="phoneCode";private final String nickName="nickName";private final String imageUrl="imageUrl";public WeChatAuthenticationFilter(String appId, String secret) {super(new AntPathRequestMatcher("/wx/login", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {String loginCode = obtainLoginCode(request)==null?"":obtainLoginCode(request).trim();String phoneCode=obtainPhoneCode(request)==null?"":obtainPhoneCode(request).trim();String nickName=obtainNickName(request)==null?"":obtainNickName(request).trim();String imageUrl=obtainImageUrl(request)==null?"":obtainImageUrl(request).trim();WechatAuthenticationToken authRequest = new WechatAuthenticationToken(loginCode,phoneCode,nickName,imageUrl);return this.getAuthenticationManager().authenticate(authRequest);}protected String obtainLoginCode(HttpServletRequest request) {return request.getParameter(loginCode);}protected String obtainPhoneCode(HttpServletRequest request){return request.getParameter(phoneCode);}protected String obtainNickName(HttpServletRequest request){return request.getParameter(nickName);}protected String obtainImageUrl(HttpServletRequest request){return request.getParameter(imageUrl);}
}

以上代码定义了一个名为WeChatAuthenticationFilter的类,它继承自AbstractAuthenticationProcessingFilter类,用于处理微信登录认证。在构造函数中,指定了请求匹配路径为"/wx/login",请求方法为POST。类中定义了四个常量:loginCode、phoneCode、nickName和imageUrl,分别表示登录码、手机号码、昵称和头像URL。

attemptAuthentication方法中,首先通过obtainLoginCode、obtainPhoneCode、obtainNickName和obtainImageUrl方法获取请求中的登录码、手机号码、昵称和头像URL,并进行了空值处理。然后将这些信息封装到WechatAuthenticationToken对象中,并通过getAuthenticationManager().authenticate方法进行认证。

4.3. 编写WeChatAuthenticationProvider

@Slf4j
public class WeChatAuthenticationProvider implements AuthenticationProvider {private final WechatConfig wechatConfig;private  RestTemplate restTemplate;private final WeChatService weChatService;private final ISysUserAuthService sysUserAuthService;public WeChatAuthenticationProvider(WechatConfig wechatConfig, WeChatService weChatService,ISysUserAuthService sysUserAuthService) {this.wechatConfig = wechatConfig;this.weChatService = weChatService;this.sysUserAuthService=sysUserAuthService;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {WechatAuthenticationToken wechatAuthenticationToken = (WechatAuthenticationToken) authentication;String loginCode = wechatAuthenticationToken.getPrincipal().toString();log.info("loginCode is {}",loginCode);String phoneCode=wechatAuthenticationToken.getPhoneCode().toString();log.info("phoneCode is {}",phoneCode);String nickName=wechatAuthenticationToken.getNickName().toString();log.info("nickName is {}",nickName);String imageUrl=wechatAuthenticationToken.getImageUrl().toString();log.info("imageUrl is {}",imageUrl);restTemplate=new RestTemplate();//获取openIdJwtUser jwtUser=null;String url = "https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={code}&grant_type=authorization_code";Map<String, String> requestMap = new HashMap<>();requestMap.put("appid", wechatConfig.getAppid());requestMap.put("secret", wechatConfig.getSecret());requestMap.put("code", loginCode);ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class,requestMap);JSONObject jsonObject= JSONObject.parseObject(responseEntity.getBody());log.info(JSONObject.toJSONString(jsonObject));String openId=jsonObject.getString("openid");if(StringUtils.isBlank(openId)) {throw new BadCredentialsException("weChat get openId error");}if(sysUserAuthService.getUserAuthCountByIdentifier(openId)>0){jwtUser = (JwtUser) weChatService.getUserByOpenId(openId);if(!jwtUser.isEnabled()){throw new BadCredentialsException("用户已失效");}return getauthenticationToken(jwtUser,jwtUser.getAuthorities());}//获取手机号第一步,获取accessTokenString accessTokenUrl="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}";Map<String, String> accessTokenRequestMap = new HashMap<>();accessTokenRequestMap.put("appid", wechatConfig.getAppid());accessTokenRequestMap.put("secret", wechatConfig.getSecret());ResponseEntity<String>  accessTokenResponseEntity = restTemplate.getForEntity(accessTokenUrl, String.class,accessTokenRequestMap);JSONObject  accessTokenJsonObject= JSONObject.parseObject(accessTokenResponseEntity.getBody());log.info(JSONObject.toJSONString(accessTokenJsonObject));String  accessToken=accessTokenJsonObject.getString("access_token");if(StringUtils.isBlank(accessToken)) {throw new BadCredentialsException("weChat get accessToken error");}//获取手机号第二部,远程请求获取手机号String pohoneUrl="https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="+accessToken+"";JSONObject phoneJson=new JSONObject();phoneJson.put("code",phoneCode);String resPhoneStr= RestTemplateUtil.postForJson(pohoneUrl,phoneJson,restTemplate);log.info(resPhoneStr);JSONObject resPhonJson= JSON.parseObject(resPhoneStr);JSONObject phoneInfo=resPhonJson.getJSONObject("phone_info");String mobile=phoneInfo.getString("phoneNumber");if(StringUtils.isBlank(mobile)){throw new BadCredentialsException("Wechat get mobile error");}jwtUser= (JwtUser) weChatService.getUserByMobile(mobile,nickName,imageUrl);sysUserAuthService.saveUserAuth(new AddUserAuthReq(jwtUser.getUid(),"wechat",openId));return getauthenticationToken(jwtUser,jwtUser.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return WechatAuthenticationToken.class.isAssignableFrom(authentication);}public WechatAuthenticationToken getauthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){WechatAuthenticationToken authenticationToken=new WechatAuthenticationToken(principal,authorities);LinkedHashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();linkedHashMap.put("principal", authenticationToken.getPrincipal());authenticationToken.setDetails(linkedHashMap);return authenticationToken;}
}

上述代码是一个自定义的认证提供者类,名为WeChatAuthenticationProvider。其主要功能是处理微信登录认证。在authenticate方法中,首先从传入的Authentication对象中提取出微信登录所需的参数,包括登录码、手机号码、昵称和头像URL。然后通过RestTemplate发送HTTP请求到微信API获取用户的openId,以验证用户身份。若成功获取openId,则检查系统中是否存在该用户的认证信息,若存在则直接返回认证token;若不存在,则继续获取用户的手机号,并根据手机号获取用户信息,并保存用户认证信息。最后,返回经过认证的token。

4.4. 编写WechatAuthenticationToken

public class WechatAuthenticationToken extends AbstractAuthenticationToken {private final Object principal;private  Object phoneCode;private Object nickName;private Object imageUrl;public WechatAuthenticationToken(String loginCode,String phoneCode,String nickName,String imageUrl) {super(null);this.principal = loginCode;this.phoneCode=phoneCode;this.nickName=nickName;this.imageUrl=imageUrl;setAuthenticated(false);}public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}public Object getPhoneCode() {return phoneCode;}public Object getNickName() {return nickName;}public Object getImageUrl() {return imageUrl;}
}

4.5. WechatConfig

@Data
@Component
@ConfigurationProperties(prefix="wechat")
public class WechatConfig {private String appid;private String secret;
}

4.6. 更新WebSecurityConfigurer

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowired@Qualifier("authUserDetailsServiceImpl")private UserDetailsService userDetailsService;@Autowiredprivate SecurOncePerRequestFilter securOncePerRequestFilter;@Autowiredprivate SecurAuthenticationEntryPoint securAuthenticationEntryPoint;@Autowiredprivate SecurAccessDeniedHandler securAccessDeniedHandler;//登录成功处理器@Autowiredprivate SecurAuthenticationSuccessHandler securAuthenticationSuccessHandler;@Autowiredprivate SecurAuthenticationFailureHandler securAuthenticationFailureHandler;//退出处理器@Autowiredprivate SecurLogoutHandler securLogoutHandler;@Autowiredprivate SecurLogoutSuccessHandler securLogoutSuccessHandler;@AutowiredBCryptPasswordEncoderUtil bCryptPasswordEncoderUtil;@Value("${wechat.appid}")private String appId;@Value("${wechat.secret}")private String secret;@AutowiredWechatConfig wechatConfig;@Autowiredprivate  WeChatService weChatService;@Autowiredprivate ISysUserAuthService sysUserAuthService;//    @Autowired
//    DynamicPermission dynamicPermission;/*** 从容器中取出 AuthenticationManagerBuilder,执行方法里面的逻辑之后,放回容器** @param authenticationManagerBuilder* @throws Exception*/@Autowiredpublic void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoderUtil);}@Overrideprotected void configure(HttpSecurity http) throws Exception {//第1步:解决跨域问题。cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();//第2步:让Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContexthttp.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().headers().cacheControl();//第3步:请求权限配置//放行注册API请求,其它任何请求都必须经过身份验证.http.authorizeRequests()
//                .antMatchers("/**").permitAll().antMatchers(HttpMethod.POST,"/sys-user/register").permitAll().antMatchers(HttpMethod.GET,"/temp/create","/department/enable-department","/instance/**","/file/download/**").permitAll().antMatchers("/css/**", "/js/**", "/images/**", "/fonts/**","/editor-app/**","/model/**","/editor/**").permitAll().antMatchers("/modeler.html/**").permitAll().antMatchers("/feign/**").permitAll()//ROLE_ADMIN可以操作任何事情.antMatchers("/v2/api-docs", "/v2/feign-docs","/swagger-resources/configuration/ui","/swagger-resources","/swagger-resources/configuration/security","/swagger-ui.html", "/webjars/**").permitAll().antMatchers(HttpMethod.POST, "/user/wx/login").permitAll().anyRequest().authenticated();//                .antMatchers("/**").hasAnyAuthority("USER","SUPER_ADMIN","ADMIN");/*由于使用动态资源配置,以上代码在数据库中配置如下:在sys_backend_api_table中添加一条记录backend_api_id=1,backend_api_name = 所有API,backend_api_url=/**,backend_api_method=GET,POST,PUT,DELETE*///动态加载资源
//                .anyRequest().access("@dynamicPermission.checkPermisstion(request,authentication)");//第4步:拦截账号、密码。覆盖 UsernamePasswordAuthenticationFilter过滤器http.addFilterAt(securUsernamePasswordAuthenticationFilter() , UsernamePasswordAuthenticationFilter.class);//第5步:拦截token,并检测。在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilterhttp.addFilterBefore(securOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);http.addFilterBefore(weChatAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//第6步:处理异常情况:认证失败和权限不足http.exceptionHandling().authenticationEntryPoint(securAuthenticationEntryPoint).accessDeniedHandler(securAccessDeniedHandler);//第7步:登录,因为使用前端发送JSON方式进行登录,所以登录模式不设置也是可以的。http.formLogin();//第8步:退出http.logout().addLogoutHandler(securLogoutHandler).logoutSuccessHandler(securLogoutSuccessHandler);}@Beanpublic WeChatAuthenticationFilter weChatAuthenticationFilter() throws Exception {WeChatAuthenticationFilter filter = new WeChatAuthenticationFilter(appId, secret);filter.setAuthenticationManager(authenticationManagerBean());filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);return filter;}/*** 手动注册账号、密码拦截器* @return* @throws Exception*/@BeanSecurUsernamePasswordAuthenticationFilter securUsernamePasswordAuthenticationFilter() throws Exception {SecurUsernamePasswordAuthenticationFilter filter = new SecurUsernamePasswordAuthenticationFilter();//成功后处理filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);//失败后处理filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);filter.setAuthenticationManager(authenticationManagerBean());return filter;}@Beanpublic WeChatAuthenticationProvider weChatAuthenticationProvider() {return new WeChatAuthenticationProvider(wechatConfig,weChatService,sysUserAuthService);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 添加微信登录认证提供者auth.authenticationProvider(weChatAuthenticationProvider());// 添加用户名密码登录认证提供者auth.authenticationProvider(daoAuthenticationProvider());}@Beanpublic DaoAuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setPasswordEncoder(new BCryptPasswordEncoder());return provider;}
}

说一个容易踩坑的地方:在Spring Security中,当你配置了自定义的认证提供者(如weChatAuthenticationProvider())来处理特定类型的认证(如微信登录),如果没有同时配置默认的认证提供者(如daoAuthenticationProvider()),则原有的基于用户名和密码的认证机制不会自动生效。这是因为Spring Security的认证机制是基于一个可配置的AuthenticationManager,它管理一个AuthenticationProvider列表,这些提供者会依次尝试认证用户提交的Authentication请求。

5. 结语

在本文中以流程讲解和代码实操讲解了如何在已有用户名和密码登录的基础上,实现微信小程序登录集成。下期将介绍基于OAuth2框架如何实现小程序登录,感兴趣的同学动动你们发财的小手点点关注吧~

 6. 参考链接 

开放能力 / 用户信息 / 手机号快速验证组件 (qq.com)

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

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

相关文章

海外媒体发稿的关键步骤和投稿策略:如何撰写高质量的新闻稿?国外软文发布平台有哪些?

发布国外新闻稿件是一个涉及多步骤的过程&#xff0c;旨在确保您的新闻稿能够有效覆盖目标受众。以下是一些关键步骤和实用的技巧&#xff0c;帮助你实现海外媒体发稿。 1. 明确目标和受众 首先&#xff0c;明确您发布新闻稿的目标&#xff0c;是为了增加品牌曝光、推出新产品…

【NVM】持久内存的架构

1 内存数据持久化 1.1 数据持久化 持久内存系统包含如下关键组件&#xff1a;微处理器、连接微处理器内存总线上的持久内存模组&#xff08;Persistent MemoryModule&#xff0c;PMM&#xff09;及持久内存上的非易失性存储介质。 使用持久内存来实现数据的持久化&#xff0c…

蓝牙(2):BR/EDR的连接过程;查询(发现)=》寻呼(连接)=》安全建立=》认证=》pair成功;类比WiFi连接过程。

4.2.1 BR/EDR 流程&#xff1a; 查询&#xff08;发现&#xff09;》寻呼&#xff08;连接&#xff09;》安全建立》认证》pair成功 4.2.1.1 查询&#xff08;发现&#xff09;流程Inquiry (discovering) 类比WiFi的probe request/response 蓝牙设备使用查询流程来发现附近的…

【Docker实操】启动redis服务

一、步骤 1、获取redis镜像 执行获取redis镜像命令&#xff1a;docker pull redis。打印镜像清单&#xff0c;可以看到新拉到的redis镜像。 2、创建redis.conf配置文件 linux主机 mkdir -p /root/www/redis/conf touch /root/www/redis/conf/redis.conf cat << EOF &…

vue contextPath的思考

先说我这边的情况&#xff0c;目前项目都是前后端分离开发的&#xff0c;上线有种部署方式&#xff0c;常见的就是前后端分开部署&#xff0c;这是比较常见的&#xff0c;我这边因客户原因&#xff0c;打包一起进行部署比较简单&#xff0c;交付技术运维部方便后期其他现场部署…

基于Matlab卷积神经网络人脸识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 人脸识别作为计算机视觉领域的关键技术之一&#xff0c;具有广泛的应用前景&#xff0c;如安全…

“高考钉子户”唐尚珺决定再战2024年高考

“高考钉子户”唐尚珺决定在2024年再次参加高考&#xff0c;这个选择确实很特别也很有趣。十几年连续参加高考&#xff0c;他已经积累了大量的备考经验和应试技巧。这样的经验对于高考辅导机构来说无疑是非常宝贵的资源&#xff0c;他如果选择去辅导机构当老师&#xff0c;应该…

文章解读与仿真程序复现思路——电力系统保护与控制EI\CSCD\北大核心《基于改进粒子滤波的锂离子电池剩余寿命预测 》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

vue 点击复制文本到剪贴板

一、首先在vue文件的template中定义复制按钮 <div size"small" v-if"item.prop jadeCode" class"cell-container"><span>{{ scope.row.jadeCode }}</span> <button click"handleCopy(scope.row.jadeCode)" clas…

告别硬编码:Spring条件注解优雅应对多类场景

一、背景 在当今的软件开发中&#xff0c;服务接口通常需要对应多个实现类&#xff0c;以满足不同的需求和场景。举例来说&#xff0c;假设我们是一家2B公司&#xff0c;公司的产品具备对象存储服务的能力。然而&#xff0c;在不同的合作机构部署时&#xff0c;发现每家公司底…

ABB 任务 模块 程序

1&#xff0c;任务由模块组成 &#xff0c; 2&#xff0c;模块分为程序模块和系统模块 3&#xff0c;可以通过新建程序模块和删除程序模块 4.可以在程序模块中构建程序 5&#xff0c;系统模块不能够被删除 6&#xff0c;main 程序主要体现在自动运行中

【Unity AR开发插件】四、制作热更数据-AR图片识别场景

专栏 本专栏将介绍如何使用这个支持热更的AR开发插件&#xff0c;快速地开发AR应用。 链接&#xff1a; Unity开发AR系列 插件简介 通过热更技术实现动态地加载AR场景&#xff0c;简化了AR开发流程&#xff0c;让用户可更多地关注Unity场景内容的制作。 “EnvInstaller…”支…

鸿蒙开发配置官方地图

一共需要配置 p12 p7b cer csr 四个文件 p12文件配置 注意创建文件名必须是.p12 到AGC创建项目 AppGallery Connect 添加自己的项目名称 我没有开启 暂时不需要 看个人需求 下载刚创建的cer证书 回到我的项目中 点击刚创建的项目 点击这里 四个文件齐全了 "metadata&qu…

Python爬取B站视频:封装一下

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…

跨境电商赛道,云手机到底能不能化繁为简?

当下国内电商背景&#xff1a; 从零售额的数据来看&#xff1a;随着互联网的普及和消费者购物习惯的改变&#xff0c;国内电商市场规模持续扩大。据相关数据显示&#xff0c;网络消费亮点纷呈&#xff0c;一季度全国网上零售额达到了3.3万亿元&#xff0c;同比增长12.4%。这表…

本地centos7+docker+ollama+gpu部署

1、一台有 NVIDIA GPU 驱动的机器 2、Docker CE安装 # 删除旧版本的 Docker&#xff08;如果存在&#xff09; sudo yum remove -y docker docker-common docker-selinux docker-engine # 安装必要的软件包&#xff1a; sudo yum install -y yum-utils device-mapper-persiste…

gpt-4o继续迭代考场安排程序 一键生成考场清单

接上两篇gpt-4o考场安排-CSDN博客&#xff0c;考场分层次安排&#xff0c;最终exe版-CSDN博客 当然你也可以只看这一篇。 今天又添加了以下功能&#xff0c;程序见后。 1、自动分页&#xff0c;每个考场打印一页 2、添加了打印试场单页眉 3、添加了页脚 第X页&#xff0c;…

基于 Java 的浏览器——JxBrowser使用分享

软件介绍 JxBrowser 是一个基于 Java 的浏览器&#xff0c;它使用 Chromium 引擎来提供高性能的网页渲染和丰富的功能。它支持多种 GUI 框架&#xff0c;如 Swing、JavaFX 和 SWT&#xff0c;使得在 Java 应用程序中嵌入浏览器组件变得简单。 JxBrowser 是一个适用于多种用途…

一维前缀和[模版]

题目链接 题目: 分析: 因为要求数组中连续区间的和, 可以使用前缀和算法注意:下标是从1开始算起的, 真正下标0的位置是0第一步: 预处理出来一个前缀和数组dp dp[i] 表示: 表示[1,i] 区间所有元素的和dp[i] dp[i-1] arr[i]例如示例一中: dp数组为{1,3,7}第二步: 使用前缀数…