当前位置: 首页 > article >正文

Apache Shiro 统一化实现多端登录(PC端移动端)

Apache Shiro 是一个强大且易用的Java安全框架,提供了身份验证、授权、密码学和会话管理等功能。它被广泛用于保护各种类型的应用程序,包括Web应用、桌面应用、RESTful服务、移动端应用大型企业级应用

需求背景

在当今数字化浪潮的推动下,我们共同见证了互联网从萌芽到繁荣的辉煌历程,随后移动互联网的异军突起,更是将信息的触角延伸至社会的每一个角落。这一系列的变革,不仅重塑了人们的生活方式,也极大地丰富了我们的数字生活体验。如今,用户接入数字世界的终端类型呈现出前所未有的多样化态势,从传统的Web端,到便捷的APP端,再到轻量级的小程序端,每一种终端都承载着不同的使用场景与用户需求,共同编织起一张错综复杂的数字网络。

面对如此多元且复杂的终端环境,登录与注销作为用户进入和退出系统的出入口,看似简单,实则不然。从单一终端的视角审视,登录与注销功能或许显得简单直接,无非是输入凭证、验证身份、完成会话的建立或终止。然而,当我们转换视角,从多端协同的宏观层面去考量时,就会发现这一功能的实现远非想象中那般轻松。

根据以往经验,面对此问题,我们可能是这样做的:

  • Web端:开发一套接口,单独维护;
  • 移动端:开发一套接口,单独维护;

当前方案功能实现无碍,但后续维护成本高昂。需求变更时需同步维护多套关联接口并开展回归测试,既耗时又易因版本差异引发非预期故障,影响系统稳定性。

需求分析

我们再来分析一下,多端会话ID在交互方式上呈现的差异化特征:

  • Web端:在常规应用场景中,会话 ID 一般会被存储在 Cookie 里,随后借助 Cookie 机制来实现交互操作。
  • 移动端:在与服务端进行交互时,采用 RESTful 接口的方式,而会话 ID 一般会被放置在请求的 Header 中,以此来实现会话的标识与传递。

若期望通过一套代码来达成多端需求的统一实现,其核心思路在于将各类相关特征进行有机整合,并依据请求类型的差异,动态地启用相应的处理机制。这一理念在逻辑上清晰明了,然而令人遗憾的是,Apache Shiro 框架原生并不支持这种实现方式

我曾查遍 Apache Shiro 官网文档,竟找不出任何关于支持移动端的任何描述。但方法总比困难多,通过阅读源码,逐步Debug,梳理出一条清晰的脉络,可以实现上述的思路。这就是开源的好处呀!

不得不说,Apache Shrio 的设计哲学真的太棒了,原理易懂,模块划分合理,它以简洁高效之姿,展现出独特的魅力与实用价值。

搞清楚这一点,你便会恍然大悟:Apache Shiro 凭借其强大的功能特性,可应用于各种需要认证与鉴权的场景,无论是传统的 Web 端,还是当下流行的移动端,亦或是便捷实用的小程序端,皆在其适用范畴之内。

建议:大家有时间,真的可以把Shiro源码跟着Debug阅读一下,必将大有脾益。那时,你不仅会愈发倾心于Apache Shiro的精巧设计,更能参透其蕴含的安全哲学。彼时,或许便能体会我此刻按捺不住的分享热忱。

相信此刻的你,已经迫不及待啦!我们一起揭开这神秘的面纱吧!从实战角度,一步步达成目标。

知晓了原理,代码实现很简单。

实战环境

  • Spring Boot 3
  • JDK 17
  • Redis

关于 Spring Boot 3 如何集成 Apache Shiro 可以参考这篇文章,本次实战,以此为基础。

SpringBoot3 集成 Shirohttps://blog.csdn.net/li277967151/article/details/140927139

实战

注:此次实战,仅展示核心Code。

完整代码,大家可以访问这个开源项目,直接Running,更有Feel

TyFast: 基于SpringBoot+Shiro搭建的快速开发平台https://gitee.com/tommycloud/TyFast

1、Yaml配置

#Shiro配置
shiro:loginUrl: /loginsuccessUrl: /indexunauthorizedUrl: /error/401.htmllogoutUrl: /logoutuserNativeSessionManager: true #false:表示基于Servlet容器 实现Session(即HttpSession)sessionManager:cookie:name: tysidpath: /

2、SpringBoot 自动装配类

package com.ty.web.spring.config;import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.ty.web.shiro.AuthenticationFilter;
import com.ty.web.shiro.AuthorizationFilter;
import com.ty.web.shiro.CookieLogoutFilter;
import com.ty.web.shiro.DistributedSessionDao;
import com.ty.web.shiro.TyWebSessionManager;
import com.ty.web.shiro.realm.NormalRealm;
import com.ty.web.shiro.realm.WithoutPasswordRealm;
import com.ty.web.spring.config.properties.ShiroProperties;
import jakarta.servlet.DispatcherType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.Ini;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.AbstractShiroWebConfiguration;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.config.IniFilterChainResolverFactory;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Map;import static com.ty.cm.constant.ShiroConstant.SESSION_TIMEOUT;
import static org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration.FILTER_NAME;
import static org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration.REGISTRATION_BEAN_NAME;/*** Shiro配置** @Author Tommy* @Date 2022/1/27*/
@Configuration
@EnableConfigurationProperties(ShiroProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
@Slf4j
public class ShiroConfig extends AbstractShiroWebConfiguration {@Value("#{ @environment['shiro.sessionManager.cookie.name'] ?: 'x-auth-token'}")private String sessionIdHeader;/*** Shiro 常规认证Realm*/@Beanpublic Realm authenticationRealm() {return new NormalRealm();}/*** Shiro 免密认证Realm*/@Beanpublic Realm withoutPasswordRealm() {return new WithoutPasswordRealm();}/*** 分布式Session Dao*/@Beanpublic SessionDAO sessionDAO() {return new DistributedSessionDao();}/*** Shiro Session Manager*/@Beanpublic SessionManager sessionManager() {return super.sessionManager();}/*** Shiro 核心过滤器*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroProperties shiroProperties) {final ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();filterFactoryBean.setSecurityManager(securityManager); // Shiro的核心安全接口,这个属性是必须的// 各URL参数filterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl()); // 登录URL,非必须的属性filterFactoryBean.setSuccessUrl(shiroProperties.getSuccessUrl()); // 登录成功后要跳转的URLfilterFactoryBean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl()); // 访问未经授权的资源时,转到的URL// 替换Shiro默认的Filter(ShiroFilter 集成了过滤器filterchain 模式,所以Shiro内部Filter不要通过SpringBoot实例化,否则就会成为全局Filter,拦截异常)filterFactoryBean.getFilters().put("authc", authenticationFilter());filterFactoryBean.getFilters().put("perms", authorizationFilter());filterFactoryBean.getFilters().put("logout", cookieLogoutFilter());// 设置鉴权规则filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition(shiroProperties));// 设置Session有效期(只有自己实现Session DAO时,才需要设置此项)// 基于Servlet容器的 Shiro Session,有效期同 HttpSessionDefaultWebSecurityManager webSecurityManager = (DefaultWebSecurityManager) securityManager;((DefaultWebSessionManager) webSecurityManager.getSessionManager()).setGlobalSessionTimeout(SESSION_TIMEOUT * 1000); // 单位:毫秒// 设置Shiro工具类,便于获取相关对象SecurityUtils.setSecurityManager(securityManager);log.info("Apache Shiro :: 初始化完成!");return filterFactoryBean;}/*** 手动配置 Shiro 核心过滤器 (建议手动配置,否则可能因SpringBoot问题,无法初始化)*/@Bean(name = REGISTRATION_BEAN_NAME)public FilterRegistrationBean<AbstractShiroFilter> filterShiroFilterRegistrationBean(ShiroFilterFactoryBean shiroFilterFactoryBean) throws Exception {FilterRegistrationBean<AbstractShiroFilter> filterRegistrationBean = new FilterRegistrationBean<>();filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR);filterRegistrationBean.setFilter((AbstractShiroFilter)shiroFilterFactoryBean.getObject());filterRegistrationBean.setName(FILTER_NAME);filterRegistrationBean.setOrder(1);return filterRegistrationBean;}/*** Shiro连接约束配置,即过滤链的定义* <b>*  <p> anon: 匿名访问</p>*	<p> authc:认证访问</p>*	<p> perms:授权访问</p>*	<p> logout:注销访问</p>* </b>*/private Map<String, String> shiroFilterChainDefinition(ShiroProperties shiroProperties) {final DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();if (StringUtils.isNotBlank(shiroProperties.getLoginUrl())) { // 登录chainDefinition.addPathDefinition(shiroProperties.getLoginUrl(), "authc");}if (StringUtils.isNotBlank(shiroProperties.getLogoutUrl())) { // 注销chainDefinition.addPathDefinition(shiroProperties.getLogoutUrl(), "logout");}// 设置无需鉴权的URLshiroProperties.getIgnoreUrls().stream().filter(url -> StringUtils.isNotBlank(url)).forEach(url -> chainDefinition.addPathDefinition(url, "anon"));// 读取鉴权配置信息if (StringUtils.isNotBlank(shiroProperties.getRules())) {final Ini ini = new Ini();ini.load(shiroProperties.getRules());Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);if (CollectionUtils.isEmpty(section)) {section = ini.getSection(Ini.DEFAULT_SECTION_NAME);}chainDefinition.addPathDefinitions(section);}log.info("Shiro::鉴权规则初始化完毕::DefaultShiroFilterChainDefinition! --> " + chainDefinition.getFilterChainMap());return chainDefinition.getFilterChainMap();}/*** 替换Shiro默认的Filter实现:认证过滤器*/@Beanpublic AuthenticationFilter authenticationFilter() {AuthenticationFilter authcFilter = new AuthenticationFilter();authcFilter.setUsernameParam("loginName");return authcFilter;}/*** 替换Shiro默认的Filter实现:鉴权过滤器*/private AuthorizationFilter authorizationFilter() {return new AuthorizationFilter();}/*** 替换Shiro默认的Filter实现:Logout过滤器*/private CookieLogoutFilter cookieLogoutFilter() {return new CookieLogoutFilter();}/*** 替换Shiro默认的 Native Session Manager*/@Overrideprotected SessionManager nativeSessionManager() {TyWebSessionManager webSessionManager = new TyWebSessionManager();webSessionManager.setSessionIdCookieEnabled(this.sessionIdCookieEnabled);webSessionManager.setSessionIdUrlRewritingEnabled(this.sessionIdUrlRewritingEnabled);webSessionManager.setSessionIdHeader(this.sessionIdHeader);webSessionManager.setSessionIdCookie(this.sessionCookieTemplate());webSessionManager.setSessionFactory(this.sessionFactory());webSessionManager.setSessionDAO(this.sessionDAO());webSessionManager.setDeleteInvalidSessions(this.sessionManagerDeleteInvalidSessions);return webSessionManager;}/*** Thymeleaf 与 Shiro 整合*/@Beanpublic ShiroDialect shiroDialect() {return new ShiroDialect();}
}

关于实现此需求,这个装配类的核心代码就2点

  • 此类需继承:AbstractShiroWebConfiguration
  • 替换Shiro默认的 Native Session Manager
    /*** 替换Shiro默认的 Native Session Manager*/@Overrideprotected SessionManager nativeSessionManager() {TyWebSessionManager webSessionManager = new TyWebSessionManager();webSessionManager.setSessionIdCookieEnabled(this.sessionIdCookieEnabled);webSessionManager.setSessionIdUrlRewritingEnabled(this.sessionIdUrlRewritingEnabled);webSessionManager.setSessionIdHeader(this.sessionIdHeader);webSessionManager.setSessionIdCookie(this.sessionCookieTemplate());webSessionManager.setSessionFactory(this.sessionFactory());webSessionManager.setSessionDAO(this.sessionDAO());webSessionManager.setDeleteInvalidSessions(this.sessionManagerDeleteInvalidSessions);return webSessionManager;}

注:为什么这两个是关键点,说来话长,这里不做阐述,若你Debug一下源码,自然分晓。

3、实现自己的认证接口Filter

package com.ty.web.shiro;import com.ty.api.log.service.LoginAuditLogService;
import com.ty.api.model.log.LoginAuditLog;
import com.ty.api.model.system.SysUser;
import com.ty.api.system.service.SysUserService;
import com.ty.cm.model.AjaxResult;
import com.ty.cm.utils.URLUtils;
import com.ty.web.push.TPush;
import com.ty.web.spring.SpringContextHolder;
import com.ty.web.utils.WebIpUtil;
import com.ty.web.utils.WebUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;import java.io.IOException;
import java.util.Date;import static com.ty.cm.constant.Numbers.ONE;
import static com.ty.cm.constant.ShiroConstant.DEFAULT_CAPTCHA_PARAM;/*** Shiro认证服务** @Author Tommy* @Date 2022/1/27*/
@Slf4j
public class AuthenticationFilter extends FormAuthenticationFilter {/** 账户业务接口 **/@Autowired@Lazyprivate SysUserService sysUserService;/** 登录日志接口 **/@Autowired@Lazyprivate LoginAuditLogService loginAuditLogService;/** TPush消息推送 **/@Autowired@Lazyprivate TPush tpush;/** "验证码"参数名称 */private String captchaParam = DEFAULT_CAPTCHA_PARAM;/*** 创建令牌*/@Overrideprotected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {String username = getUsername(request);String password = getPassword(request);boolean rememberMe = isRememberMe(request);String host = getHost(request);String captcha = getCaptcha(request);return new com.ty.web.shiro.AuthenticationToken(username, password, rememberMe, host, captcha);}/*** 未经认证时访问系统在此拦截*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {final boolean isLoginRequest = super.isLoginRequest(request, response);if (!isLoginRequest && WebUtil.isAjax()) { // 登录URL不能拦截WebUtil.sendError(WebUtils.toHttp(response), HttpServletResponse.SC_UNAUTHORIZED);return false;}return super.onAccessDenied(request, response);}/*** 登录失败的回调函数*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ex, ServletRequest request, ServletResponse response) {final boolean isAjax = WebUtil.isAjax();/** 转换标准异常为自定义异常(因框架架构设计问题,只在多Realm情况下,才需要此操作)*/if (token instanceof com.ty.web.shiro.AuthenticationToken) {com.ty.web.shiro.AuthenticationToken authenticationToken = (com.ty.web.shiro.AuthenticationToken) token;ex = null != authenticationToken.getAex()? authenticationToken.getAex() : ex;}try {WebUtil.writeJSON(WebUtils.toHttp(response), AjaxResult.warn(SpringContextHolder.getMessage(ex.getMessage())));} catch (IOException ioe) {log.error(ioe.getMessage(), ioe);} finally {log.warn("登录校验失败::" + (isAjax? "异步":"同步") + "::" + ex.getMessage());}return !isAjax;}/*** 登录成功的回调函数*/@Overrideprotected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {final boolean isAjax = WebUtil.isAjax();// 进入系统前的业务处理this.postHandle(subject, isAjax, request, response);// 输出成功信息try {WebUtil.writeJSON(WebUtils.toHttp(response), AjaxResult.success(subject.getSession().getId()));} catch (IOException ioe) {log.error(ioe.getMessage(), ioe);}return !isAjax? super.onLoginSuccess(token, subject, request, response) : !isAjax;}/*** 获取验证码** @param request* @return 验证码*/protected String getCaptcha(ServletRequest request) {return WebUtils.getCleanParam(request, captchaParam);}/*** 认证后,进入系统前的处理** @param subject* @param isAjax* @throws Exception*/public void postHandle(Subject subject, boolean isAjax, ServletRequest request, ServletResponse response) throws Exception {String loginIp = WebIpUtil.getClientIP();String domain = URLUtils.getPrimaryDomain(WebUtil.getDomain(), true);final SysUser account = (SysUser) subject.getPrincipal();log.info(account.getLoginName() + " 登录成功::" + (isAjax? "异步":"同步") + " :: From " + loginIp);// 此处可写业务代码// 如:获取员工信息等,可在账户表中,添加辅助字段,用于存储业务数据// ......// 更新用户的登录信息(IP & 登录时间)SysUser sysUser = new SysUser();sysUser.setUserId(account.getUserId());sysUser.setLoginTime(new Date());sysUser.setLoginIp(loginIp);sysUserService.update(sysUser);// 记录登录日志loginAuditLogService.save(new LoginAuditLog(account.getLoginName(), loginIp, WebUtil.getUserAgent(), ONE));// 实现登录互踢boolean result = sysUserService.kickOut(account, subject.getSession().getId().toString());if (result) { // 将下线消息通知到同账户的其它客户端tpush.kickOut(account.getLoginName());}}
}

关于实现此需求,核心代码如下:

    /*** 登录成功的回调函数*/@Overrideprotected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {final boolean isAjax = WebUtil.isAjax();// 进入系统前的业务处理this.postHandle(subject, isAjax, request, response);// 输出成功信息try {WebUtil.writeJSON(WebUtils.toHttp(response), AjaxResult.success(subject.getSession().getId()));} catch (IOException ioe) {log.error(ioe.getMessage(), ioe);}return !isAjax? super.onLoginSuccess(token, subject, request, response) : !isAjax;}

此段代码,当移动端以异步请求登录成功后,服务端会将Session ID返回。而Web端登录成功后,走Shiro原生逻辑。

4、实现自己的Logout Filter

package com.ty.web.shiro;import com.ty.cm.model.AjaxResult;
import com.ty.cm.utils.URLUtils;
import com.ty.web.utils.WebUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.util.WebUtils;/*** 基于Cookie机制的注销登录服务** @Author Tommy* @Date 2022/1/27*/
public class CookieLogoutFilter extends LogoutFilter {/*** 注销登录业务逻辑处理*/@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response)throws Exception {getSubject(request, response).logout(); // Shiro内部实现// Cookie 登出处理String domain = URLUtils.getPrimaryDomain(WebUtil.getDomain(), true);WebUtil.removeAllCookie((HttpServletRequest) request, (HttpServletResponse) response, domain);// 登出后的前端交互if (WebUtil.isAjax()) {WebUtil.writeJSON(WebUtils.toHttp(response), AjaxResult.success());} else {issueRedirect(request, response, getRedirectUrl());}return false;}
}

5、【核心】实现自己的Web Session Manager

package com.ty.web.shiro;import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;import java.io.Serializable;/*** 增强 Web Session Manager,支持从Header中获取Session ID** @Author Tommy* @Date 2025/3/11*/
@Data
public class TyWebSessionManager extends DefaultWebSessionManager {private String sessionIdHeader;@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response) {Serializable id = super.getSessionId(request, response);// 若 Shiro 原生获取不到SessionID,则从Header中尝试获取HttpServletRequest httpRequest = (HttpServletRequest) request;String sessionId = httpRequest.getHeader(sessionIdHeader);if (StringUtils.isNotBlank(sessionId)) {id = sessionId;}return id;}
}

若Debug源码后,你会发现,这段代码实现,其实是对原生Shiro的补充,以支持移动端场景。

经过上述的5个步骤,我们的代码工作就完成了,是不是很简单呢。若你想知晓,为什么是这么写,那就只能Debug源码喽!因为这个事情,真的不太好通过Blog文字的方式,讲清楚呢!

测试

1、移动端

  • 登录接口测试

  • 调用数据接口测试

  • 注销接口测试

2、Web端

  • 登录接口测试

  • 注销接口测试

结论

通过上述测试可知,我们通过统一的接口,完美同时支持Web端与移动端,成功达成了一套代码适配多平台的高效解决方案。

此刻,你是否如拨云见日般,心中豁然开朗?是否恍然发觉,这看似棘手的难题,实则并不繁杂。只要思路如灵动的丝线般清晰穿梭,Coding便会如行云流水般简单自然。

 

至此分享结束!

Enjoy It! 

相关文章:

Apache Shiro 统一化实现多端登录(PC端移动端)

Apache Shiro 是一个强大且易用的Java安全框架&#xff0c;提供了身份验证、授权、密码学和会话管理等功能。它被广泛用于保护各种类型的应用程序&#xff0c;包括Web应用、桌面应用、RESTful服务、移动端应用和大型企业级应用。 需求背景 在当今数字化浪潮的推动下&#xff…...

NAT—地址转换(实战篇)

一、实验拓扑&#xff1a; 二、实验需求&#xff1a; 1.实现内网主机访问外网 2.实现外网客户端能够访问内网服务器 三、实验思路 1.配置NAT地址池实现内网地址转换成公网地址&#xff0c;实现内网主机能够访问外网。 2.配置NAT Sever实现公网地址映射内网服务器地址&…...

用HTML和CSS生成炫光动画卡片

这个效果结合了渐变、旋转和悬浮效果的炫酷动画示例&#xff0c;使用HTML和CSS实现。 一、效果 二、实现 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport&quo…...

Vue 3 + Composition API + Vite + Pinia + Element Plus 构建项目的完整指南

以下是使用 Vue 3 + Composition API + Vite + Pinia + Element Plus 构建项目的完整指南,包含 TypeScript 支持配置: 1. 创建项目 使用 Vite 初始化项目 npm create vite@latest my-vue-app -- --template vue-ts如果不需要 TypeScript,使用 --template vue 进入项目目录…...

FPGA_YOLO(三)

上一篇讲的是完全映射&#xff0c;也就是block中的所包含的所有的卷积以及归一&#xff0c;池化卷积 举例总共6个等都在pl侧进行处理&#xff08;写一个top 顶层 里面conv 1 bn1 relu1 pool1 conv1*1 conv 2 bn2 relu2 pool2 conv1*1 ....总共6个 &#xff09;&#xff0c;…...

使用 SQL CTE(公共表表达式)优化数据查询的实践

目录 一、背景 二、什么是 CTE&#xff1f; 三、CTE 的基本结构 四、示例分析 五、CTE 的作用 六、优势分析 一、背景 在数据分析和数据库管理中&#xff0c;SQL 查询的效率和可读性是至关重要的。随着数据量的不断增加&#xff0c;复杂的查询变得越来越难以管理和理解。…...

旅游CMS选型:WordPress、Joomla与Drupal对比

内容概要 在旅游行业数字化转型进程中&#xff0c;内容管理系统&#xff08;CMS&#xff09;的选择直接影响网站运营效率与用户体验。WordPress、Joomla和Drupal作为全球主流的开源CMS平台&#xff0c;其功能特性与行业适配性存在显著差异。本文将从旅游企业核心需求出发&…...

全面适配iOS 18.4!通付盾加固产品全面升级,护航App安全上架

引言&#xff1a; 苹果官方新规落地&#xff01; 自2025年4月24日起&#xff0c;所有提交至App Store Connect的应用必须使用Xcode 16或更高版本构建&#xff0c;否则将面临审核驳回风险&#xff01;Beta版iOS 18.4、iPadOS 18.4现已推出&#xff0c;通付盾iOS加固产品率先完成…...

bash 和 pip 是两种完全不同用途的命令,分别用于[系统终端操作]和[Python 包管理]

bash 和 pip 是两种完全不同用途的命令&#xff0c;分别用于 系统终端操作 和 Python 包管理。以下是它们的核心区别、用法及常见场景对比&#xff1a; 1. 本质区别 特性bashpip类型Shell 命令解释器&#xff08;一种脚本语言&#xff09;Python 包管理工具作用执行系统命令、…...

SQL 通用表表达式(CTE )

目录 概念&#xff1a;CTE&#xff1a; Common table Expression CTE 语法 CTE Demo 概念&#xff1a;CTE&#xff1a; Common table Expression 通用表表达式&#xff08;CTE&#xff09;是SQL中用于简化复杂查询的工具&#xff0c;第一次上线于SQL Server 2005。 CTE提供…...

一台电脑最多能接几个硬盘?

在使用电脑时&#xff0c;硬盘空间不够是许多用户都会遇到的问题。无论是摄影师、剪辑师等需要大量存储空间的专业人士&#xff0c;还是游戏玩家、数据备份爱好者&#xff0c;都可能希望通过增加硬盘来扩展存储容量。然而&#xff0c;一台电脑究竟最多能接多少个硬盘&#xff1…...

MATLAB中iscell函数用法

目录 语法 说明 示例 确定数组是否为元胞数组 iscell函数的功能是确定输入是否为元胞数组。 语法 tf iscell(A) 说明 如果 A 是元胞数组&#xff0c;则 tf iscell(A) 返回 1 (true)。否则&#xff0c;将返回 0 (false)。 示例 确定数组是否为元胞数组 创建一个元胞数…...

【玩转全栈】---- Django 基于 Websocket 实现群聊(解决channel连接不了)

学习视频&#xff1a; 14-11 群聊&#xff08;一&#xff09;_哔哩哔哩_bilibili 目录 Websocket 连接不了&#xff1f; 收发数据 断开连接 完整代码 聊天室的实现 聊天室一 聊天室二 settings 配置 consumer 配置 多聊天室 Websocket 连接不了&#xff1f; 基于这篇博客&…...

如何快速解决django报错:cx_Oracle.DatabaseError: ORA-00942: table or view does not exist

我们在使用django连接oracle进行编程时&#xff0c;使用model进行表映射对接oracle数据时&#xff0c;默认表名组成结构为&#xff1a;应用名_类名&#xff08;如&#xff1a;OracleModel_test&#xff09;&#xff0c;故即使我们库中存在表test&#xff0c;运行查询时候&#…...

Selenium之简介

Selenium简介 首先&#xff0c;让我们看看官网是怎么定义的 Selenium是一个支持web浏览器自动化的一系列工具和库的综合项目&#xff0c;提供了扩展来模拟用户和浏览器的交互&#xff0c;用于扩展浏览器分配的分发服务器&#xff1b;用于W3C WebDriver规范的基础架构 其实&a…...

pip 安装某个包之后,Jupyter Lab仍旧显示包冲突;例如:Numba needs NumPy 2.1 or less. Got NumPy 2.2.

异常提示 Numba needs NumPy 2.1 or less. Got NumPy 2.2. --------------------------------------------------------------------------- ImportError Traceback (most recent call last) Cell In[8], line 53 import pywt4 import matplot…...

本地安装git

下载git 通过官网 下载 &#xff1a;Git - Downloading Package 若此页面无法直达&#xff0c;请删掉download/win尝试 2.双击运行安装 选择安装目录&#xff1a; 选择配置&#xff0c;默认不动 git安装目录名 默认即可 Git 的默认编辑器&#xff0c;建议使用默认的 Vim 编辑器…...

小程序内表格合并功能实现—行合并

功能介绍&#xff1a;支付宝小程序手写表格实现行内合并&#xff0c;依据动态数据自动计算每次需求合并的值&#xff0c;本次记录行内合并&#xff0c;如果列内合并&#xff0c;同理即可实现 前端技术&#xff1a;grid布局 display&#xff1a;grid 先看实现效果: axml&…...

SSE协议介绍和python实现

概述&#xff1a; SSE&#xff08;Server-Sent Events&#xff09;协议是一种允许服务器向客户端实时推送更新的技术&#xff0c;基于HTTP协议&#xff0c;常用于实时数据推送特点&#xff1a; 单向通信&#xff1a;服务器向客户端推送数据&#xff0c;客户端无法发送数据。基…...

甘肃旅游服务平台+论文源码视频演示

4 系统设计 4.1系统概要设计 甘肃旅游服务平台并没有使用C/S结构&#xff0c;而是基于网络浏览器的方式去访问服务器&#xff0c;进而获取需要的数据信息&#xff0c;这种依靠浏览器进行数据访问的模式就是现在用得比较广泛的适用于广域网并且没有网速限制要求的小程序结构&am…...

Spring Boot 3虚拟线程的使用

在Spring Boot非Web应用中&#xff0c;使用虚拟线程时程序提前终止的问题及解决方案&#xff0c;可以通过以下步骤深入理解和验证&#xff1a; 问题根源分析 JVM退出机制 Java中&#xff0c;当所有非守护线程结束时&#xff0c;JVM会立即退出。即使存在正在运行的守护线程&…...

3、pytest实现参数化

在 pytest 中&#xff0c;参数化&#xff08;parametrization&#xff09;是一种强大的功能&#xff0c;可以让你用不同的输入数据重复执行同一个测试函数。这种功能非常有用&#xff0c;可以帮助你显著减少重复代码并提高测试覆盖率。 参数化的主要作用是&#xff1a; 测试多…...

【解决】Linux命令报错:Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64

报错命令 yum install zabbix-web-mysql-scl zabbix-apache-conf-scl centos使用scl切换软件版本时提示Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64 报错原因 CentOS7的SCL源在2024年6月30日停止维护了。 当scl源里面默认使用了centos官方的地址&#x…...

WebRTC中音视频服务质量QoS之FEC+NACK调用流程

WebRTC中音视频服务质量QoS之FECNACK调用流程 WebRTC中音视频服务质量QoS之FECNACK调用流程 WebRTC中音视频服务质量QoS之FECNACK调用流程前言一、WebRTC中FEC基础原理1. FEC基础操作 异或操作XOR2、 FEC中 行向和纵向 计算3、 WebRTC中 媒体包分组和生成FEC的包数① kFecRateT…...

神经网络知识点整理

目录 ​一、深度学习基础与流程 二、神经网络基础组件 三、卷积神经网络&#xff08;CNN&#xff09;​编辑 四、循环神经网络&#xff08;RNN&#xff09;与LSTM 五、优化技巧与调参 六、应用场景与前沿​编辑 七、总结与展望​编辑 一、深度学习基础与流程 机器学习流…...

远程办公新体验:用触屏手机流畅操作电脑桌面

在数字化浪潮的推动下&#xff0c;远程办公已从“应急选项”转变为职场常态。无论是居家隔离、差旅途中&#xff0c;还是咖啡厅临时办公&#xff0c;高效连接公司电脑的需求从未如此迫切。然而&#xff0c;传统的远程控制软件常因操作复杂、画面卡顿或功能限制而影响效率。如今…...

【面试八股】:常见的锁策略

常见的锁策略 synchronized &#xff08;标准库的锁不够你用了&#xff09;锁策略和 Java 不强相关&#xff0c;其他语言涉及到锁&#xff0c;也有这样的锁策略。 1. 悲观锁&#xff0c;乐观锁&#xff08;描述的加锁时遇到的场景&#xff09; 悲观锁&#xff1a;预测接下来…...

【python】OpenCV—Hand Detection

文章目录 1、功能描述2、代码实现3、效果展示4、完整代码5、参考6、其它手部检测和手势识别的方案 更多有趣的代码示例&#xff0c;可参考【Programming】 1、功能描述 基于 opencv-python 和 mediapipe 进行手部检测 2、代码实现 导入必要的库函数 import cv2 import media…...

es6的100个问题

基础概念 解释 let、const 和 var 的区别。什么是块级作用域&#xff1f;ES6 如何实现它&#xff1f;箭头函数和普通函数的主要区别是什么&#xff1f;解释模板字符串&#xff08;Template Literals&#xff09;的用途&#xff0c;并举例嵌套变量的写法。解构赋值的语法是什么…...

【Git 常用指令速查表】

Git 常用指令速查表 Git 常用指令速查表目录1. 初始化仓库2. 提交代码流程3. 分支管理4. 远程仓库操作5. 撤销操作6. 查看状态与日志7. 其他实用指令完整操作示例常用场景速查表 Git 常用指令速查表 目录 初始化仓库提交代码流程分支管理远程仓库操作撤销操作查看状态与日志其…...