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

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…...

es新增运算符

?? ( 空值合并运算符) ?. (可选链式运算符) ?? (空值合并赋值操作符) // ?? ( 空值合并运算符)&#xff1a;这个运算符主要是左侧为null和undefined&#xff0c;直接返回右侧值 let result value ?? 默认值;. ??&#xff08;空值合并运算符&#xff09; ✅ 用于…...

数据库三级填空+应用(2)

sysadmin、dbcreator 数据是面向主题的&#xff08;2&#xff09;、集成的、非易失的、随时间不断变化的数据集合&#xff0c; 数据字典 【答案】完整性约束 数据模型成分 33【解析】顺序图主要用于描述系统内对象之间的消息发送和接收序列。 34如果把舍弃的元组也保存在结果关…...

贪心算法经典应用:最优答疑调度策略详解与Python实现

目录 引言&#xff1a;从现实场景到算法设计 一、问题背景与数学建模 1.1 现实场景抽象 1.2 时间线分析 二、贪心策略的数学证明与选择依据 2.1 贪心选择性质 2.2 证明过程 三、算法实现与代码解析 3.1 算法步骤分解 3.2 代码亮点解析 四、测试案例与结果验证 4.1 …...

把手搭建vue前后端管理系统-TAB标签通过pinia来进行管理(二十六)

目标&#xff1a;通过pinia的store来进行组件状态的统一管理&#xff0c;这样大家都可以共用到这个组件的状态信息&#xff0c;就可以实现组件的联动 一、添加侧边栏菜单的点击事件&#xff1a; 1、CommonAside.vue里面添加click的事件 <el-menu-itemv-for"item in …...

Python与数据库

目录 一、数据库 1、数据库的概念 2、数据库的表 3、字段详解 二、SQL数据库语句 1、了解SQL命令 2、CREATE命令 3、INSERT命令 三、数据库和SQL命令 四、数据库的查询与修改 1、SELECT命令 2、UPDATE命令 3、DELETE命令 4、DROP TABLE 5、SQL的注意事项 五、处…...

MyBatis中mapper.xml 的sql映射规则

一、SQL 映射文件核心元素 MyBatis 映射文件的顶级元素&#xff08;按定义顺序&#xff09;&#xff1a; cache&#xff1a;命名空间的缓存配置。cache-ref&#xff1a;引用其他命名空间的缓存。resultMap&#xff1a;自定义结果集映射。sql&#xff1a;可重用的 SQL 片段。i…...

ubuntu22.04安装搜狗输入法保姆教程~

一、添加中文语言支持 1.首先打开设置,找到Language and Region 2.点击Manage Installed Languages 3.点击 Install/Remove Languages... 4.选中Chinese (simplified),点击Apply...

Jenkins 配置python项目和allure

Jenkins新建项目 新建ry-api-auto-test。 添加项目描述&#xff0c;选择gitee令牌。 源码管理&#xff0c;设置仓库地址和凭证。参考我上一篇文章的链接&#xff1a;配置gitee私人令牌和凭证 构建步骤&#xff0c;因为我Jenkins部署在Windows&#xff0c;因此选择batch。…...

keda基于postgresql伸缩dify-api服务

1 概述 dify-api使用postgresql来存储数据&#xff0c;在dify控制台每新建一个聊天机器的聊天框&#xff0c;就会在conversations表里新插入一条记录&#xff0c;并且不断地更新字段updated_at&#xff0c;示例如下&#xff1a; dify# select * from conversations limit 1; …...

蓝桥杯 拼正方形

问题描述 小蓝正在玩拼图游戏。他有&#xff1a; 7385137888721 个 22 的方块10470245 个 11 的方块 他需要从中挑出一些方块来拼出一个正方形。 例如&#xff1a; 用 3 个 22 和 4 个 11 方块可以拼出一个 44 的正方形&#xff1b;用 9 个 22 方块可以拼出一个 66 的正方…...

failed to load steamui.dll”错误:Steam用户的高频崩溃问题解析

当你满心欢喜地双击 Steam 图标&#xff0c;准备进入游戏世界时&#xff0c;屏幕上突然弹出 “failed to load steamui.dll” 的刺眼提示——这是全球数百万 Steam 用户最不愿见到的错误之一。作为 Steam 客户端的核心界面动态链接库文件&#xff0c;steamui.dll 的缺失或损坏会…...

Django之旅:第六节--mysql数据库操作增删改查(二)

前提条件(models.py已经设置好&#xff09;&#xff1a; from django.db import mmodelsclass UserInfo(models.Model):namemodels.CharFIeld(max_length32)passwordmodels.CharFIeld(max_length64)#agemodels.IntegerFIeld()操作数据语法&#xff08;在views.py文件&#xff0…...

6. 使用VUE实现前端页面的分级嵌套

1. 说明 在UI设计中&#xff0c;页面中有些部分的占用空间位置是固定不动&#xff0c;有些部分的区域是根据情况进行动态切换的。比如&#xff0c;一个网页的菜单栏和主题内容展示&#xff0c;往往菜单栏区域的导航按钮占用的空间是固定不动的&#xff0c;当用户点击不同按钮时…...

(UI自动化测试web端)第三篇:元素的常用操作方法_浏览器操作

模拟浏览器的常见操作。 1、最大化浏览器窗口 driver.maximize_window()2、浏览器后退、前进、刷新、关闭、退出 # 调用浏览器的后退 driver.back() # 调用浏览器的前进 driver.forward() # 刷新页面 driver.refresh() # 关闭当前窗口 driver.close() # 退出浏览器 driver.q…...

Ubuntu软件包离线下载安装

1、下载软件包tcpd&#xff0c;并在/var/cache/apt/archives目录中查看。 rooteducoder:~# apt-get install -d tcpd Reading package lists... Done Building dependency tree Reading state information... Done The following NEW packages will be installed:tcpd …...

第十节 MATLAB逻辑运算

MATLAB逻辑运算都是针对元素的操作&#xff0c;运算结果是特殊的逻辑数组&#xff1b;在逻辑分析时&#xff0c;逻辑&#xff08;真&#xff09;用1表示&#xff0c;逻辑假用0表示&#xff0c;逻辑运算中所有的非零元素作为1处理。 注意&#xff1a; 使用MATLAB逻辑运算时的语…...

初识哈希表

一、题意 给定一个整数数组 nums 和一个目标值 target&#xff0c;要求你在数组中找出和为目标值的那两个整数&#xff0c;并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素不能使用两遍。 示例&#xff1a; 给定 nums [2, 7, …...

Ajax与Axios,以及Apifox的入门使用

Ajax与Axios&#xff0c;以及Apifox的入门使用 作者&#xff1a;blue 时间&#xff1a;2025.3.20 文章目录 Ajax与Axios&#xff0c;以及Apifox的入门使用1.Ajax2.Axios3.Apifox的基本使用内容Path 参数定义语法用途 Query 参数定义语法用途 1.Ajax 概念&#xff1a;Asynchr…...

jmm-java内存模型

java内存模型----底层原理 底层原理 从Java代码到最终执行的CPU指令的流程&#xff1a; 最开始&#xff0c;我们编写的Java代码&#xff0c;是*.java文件在编译&#xff08;javac命令&#xff09;后&#xff0c;从刚才的*.java文件会变出一个新的Java字节码文件&#xff08;…...

机器学习——KNN数据均一化

在KNN&#xff08;K-近邻&#xff09;算法中&#xff0c;数据均一化&#xff08;归一化&#xff09;是预处理的关键步骤&#xff0c;用于消除不同特征量纲差异对距离计算的影响。以下是两种常用的归一化操作及其核心要点&#xff1a; 质押 一 、主要思想 1. 最值归一化&#…...

页面元素内容太长,给元素添加title

一、需求 页面元素内容太长&#xff0c;给元素添加title 二、实现 1、直接使用title属性 <div target"_blank" class"text-overflow" title"叉车司机">叉车司机</div> 2、使用tdesign的Popup 弹出层 <t-popup>触发元素&…...

【Git多分支使用教程】

Git多分支使用教程 Git多分支使用手册目录多分支只拉取一个多分支拉取指定几个步骤 1&#xff1a;克隆第一个分支步骤 2&#xff1a;获取其他分支 常见问题与解决方法1. 错误&#xff1a;origin/分支名 is not a commit2. 分支名称冲突3. --single-branch 限制 总结 Git多分支使…...

【408--复习笔记】数据结构

【408--复习笔记】数据结构 1.绪论数据结构基本概念• 请简述数据结构的定义。• 数据结构中数据、数据元素、数据项、数据对象的区别是什么&#xff1f; 算法相关• 什么是算法&#xff1f;算法的五个重要特性是什么&#xff1f;• 如何理解算法的时间复杂度和空间复杂度&…...

使用 Vite 提升前端开发体验:入门与配置指南

在现代前端开发中&#xff0c;构建工具的选择对开发效率和项目性能有着至关重要的影响。Vite 是一个新兴的前端构建工具&#xff0c;由 Vue.js 的作者尤雨溪开发&#xff0c;旨在通过利用现代浏览器的原生 ES 模块特性&#xff0c;提供更快的开发服务器启动速度和更高效的热更新…...

WPS JS宏编程教程(从基础到进阶)--第二部分:WPS对象模型与核心操作

第二部分&#xff1a;WPS对象模型与核心操作 WPS对象的属性、方法、集合 工作簿对象常用表达方式工作表对象常用表达方式单元格对象常用表达方式 单元格操作实战 单元格复制与重定位单元格偏移与尺寸调整 颜色设置专题 索引颜色与RGB颜色按条件动态设置单元格颜色 第二部分&…...

瑞数信息《BOTS自动化威胁报告》正式发布

在数字化时代&#xff0c;BOTS自动化攻击如同一场无声的风暴&#xff0c;正以前所未有的态势席卷全球网络空间。为了让各行业更好地应对自动化威胁挑战&#xff0c;瑞数信息作为BOTS自动化攻击防护领域的专业厂商&#xff0c;多年来持续输出BOTS自动化威胁报告&#xff0c;为各…...

【NUUO 摄像头】(弱口令登录漏洞)

漏洞简介&#xff1a;NUUO 是NUUO公司的一款小型网络硬盘录像机设备。 NUUO NVRMini2 3.0.8及之前版本中存在后门调试文件。远程攻击者可通过向后门文件handle_site_config.php发送特定的请求利用该漏洞执行任意命令。 1.Fofa搜索语句&#xff1a; 在Fofa网站&#xff0c;搜索&…...

Android系统的安全问题 - Linux的能力模型(Capability)和 SELinux 的区别

Linux 的能力模型&#xff08;Capabilities&#xff09;和 SELinux 是两种不同的安全机制&#xff0c;虽然它们都用于增强 Linux 系统的安全性&#xff0c;但它们的实现方式和目标有所不同。 1. Linux Capabilities&#xff08;能力模型&#xff09; 作用&#xff1a;传统的 …...

Rust安装并配置配置vscode编译器

一. 下载rustup-init.exe rust下载网址&#xff1a;Getting started - Rust Programming Language 根据系统&#xff0c;选择适合的exe文件 我选择的的是右边64bit的 打开下载的文件 输入1&#xff0c;回车 二. Visual C 安装 自动下载安装vs 等待安装完毕 三. Rust 安装…...

Spring Boot响应压缩配置与优化

一、核心工作机制 1.1 自动协商触发条件 Spring Boot的响应压缩功能基于智能协商机制&#xff0c;需同时满足以下条件方可触发&#xff1a; 客户端支持&#xff1a;请求头包含Accept-Encoding: gzip/deflate数据量阈值&#xff1a;响应体大小超过预设值&#xff08;默认2KB&…...

el-select开启filterable模式,限制输入框输入类型

遇到el-select开启filterable模式查询&#xff0c;下拉框内容是文字与数字组合版&#xff0c;导致校验不准&#xff0c;且没有属性能直接限制focus输入的内容&#xff0c;这时候可以用自定义属性来解决 实例&#xff1a;&#xff08;以只能输入数字为例&#xff09; <el-for…...

创建login.api.js步骤和方法

依次创建 login.api.js、home.api.js...... login.api.js、home.api.js 差不多 导入到 main.js main.js 项目中使用...

在线运行vscode

安装 https://github.com/coder/code-server?utm_sourcesyndication&pubDate20250317 运行前预览脚本 curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run运行脚本 curl -fsSL https://code-server.dev/install.sh | sh其他 可以通过后台服务运行&am…...

【Nginx】可以做哪些优化?

一、配置文件优化 1.1 性能优化 开启网页压缩 gzip on;设置网页缓存时间expires 缓存时间;设置连接保持超时keepalive_timeout 服务端超时时间 客户端超时时间;设置连接保持最大请求数keepalive_requests设置工作进程数 worker_processes 与服务器CPU数量…...

springboot在feign和线程池中使用TraceId日志链路追踪(最终版)-2

文章目录 简述问题feign调用时给head加入traceIdFeignConfig配置FeignConfig 局部生效feign拦截器和配置合并为一个文件&#xff08;最终版&#xff09;feign异步调用拦截器配置[不常用] 使用TTL自定义线程池为什么需要TransmittableThreadLocal&#xff1f; 总结参考和拓展阅读…...

datawhale组队学习-大语言模型-task5:主流模型架构及新型架构

目录 5.3 主流架构 5.3.1 编码器-解码器架构 5.3.2 因果解码器架构 5.3.3 前缀解码器架构 5.4 长上下文模型 5.4.1 扩展位置编码 5.4.2 调整上下文窗口 5.4.3 长文本数据 5.5 新型模型架构 5.5.1 参数化状态空间模型 5.5.2 状态空间模型变种 5.3 主流架构 在预训…...

《Matplotlib三维可视化工业实践——从分子模拟到流体力学》

目录 ​编辑 一、工业三维可视化挑战 1.1 典型工业场景需求 1.2 技术痛点分析 二、Matplotlib三维可视化基础 2.1 三维坐标体系构建 2.2 核心三维绘图API 三、分子模拟可视化实战 3.1 晶体结构渲染 3.2 分子轨迹动态演示 四、流体力学场数据优化渲染 4.1 矢量场高效…...

【neo4j数据导出并在其他电脑导入】

停止服务 neo4j stop 导出 neo4j-admin database dump neo4j --to-path"C:\Users\12901\Downloads\test folder" 导入 将 .dump 文件放在一个目录中 mkdir /root/dump-directory mv /root/neo4j.dump /root/dump-directory/ 使用包含 .dump 文件的目录路径作为 …...

多智能体融合(Multi-Agent Fusion)

多智能体融合&#xff08;Multi-Agent Fusion&#xff09;是指在多智能体系统&#xff08;MAS, Multi-Agent System&#xff09;中&#xff0c;多个智能体&#xff08;Agent&#xff09;通过协作、竞争或共享信息&#xff0c;实现全局最优的智能决策和任务执行。该方法广泛应用…...

状态模式(State Pattern)

状态模式&#xff08;State Pattern&#xff09; 如果任务的执行过程是有多个不同状态的&#xff08;比如初始化、运行中、完成等&#xff09;&#xff0c;你可以使用状态模式。每个状态可以有不同的行为&#xff0c;使得任务的状态管理更加清晰和可维护。 示例&#xff1a; …...

Linux网站搭建(新手必看)

1.宝塔Linux面板的功能 宝塔面板是一款服务器管理软件&#xff0c;可以帮助用户建立网站&#xff0c;一键配置服务器环境&#xff0c;使得用户通过web界面就可以轻松的管理安装所用的服务器软件。 2. 宝塔Linux面板的安装 宝塔官网地址&#xff1a;宝塔面板 - 简单好用的Linu…...

JavaEE进阶---Mybatis(预编译SQL即时SQL动态SQL标签池化技术说明)

文章目录 1.经典面试题&#xff08;#{}和${}的区别&#xff09;1.1关于#1.2关于$1.3情况下需要使用$ 2.数据库连接池2.1池化技术图解 3.动态SQL3.1if标签的使用3.2where标签的使用3.3set标签的使用 1.经典面试题&#xff08;#{}和${}的区别&#xff09; 1.1关于# 预编译SQL&a…...

Object.defineProperty()Proxy详解(Vue23数据劫持实现)

底层原理&#x1f447;&#x1f3ff; 总结一下&#xff0c;结构应该包括&#xff1a; 1. 方法的基本作用和参数。 2. 数据描述符和存取描述符的区别。 3. 属性定义的内部处理流程。 4. 在Vue中的应用实例。 5. 常见错误和正确实践。 每个部分都要结合搜索结果的信息&…...

网页的性能优化

面试中如何回答"前端性能优化"问题 在面试中回答性能优化问题时&#xff0c;建议采用结构化表达方式&#xff0c;展示你的系统化思维和实战经验。以下是一个推荐的回答框架&#xff1a; 1. 开场概述 “前端性能优化是一个系统工程&#xff0c;我通常会从加载性能、…...

Vue 3中的Teleport:超越组件边界的渲染

Vue 3引入了许多新特性&#xff0c;其中之一便是Teleport。它为开发者提供了一种强有力的方式来控制组件的渲染位置&#xff0c;使得我们可以将组件的内容“传送”到DOM树的任何地方&#xff0c;而不仅仅局限于其父级组件的边界内。这在创建模态框、通知系统或任何需要脱离当前…...

JVM垃圾回收笔记01-垃圾回收算法

文章目录 前言1. 如何判断对象可以回收1.1 引用计数法1.2 可达性分析算法查看根对象哪些对象可以作为 GC Root ?对象可以被回收&#xff0c;就代表一定会被回收吗&#xff1f; 1.3 引用类型1.强引用&#xff08;StrongReference&#xff09;2.软引用&#xff08;SoftReference…...

3.26学习总结

今天主要学习了内部类&#xff0c;但总感觉有点混乱&#xff0c;和之前的抽象啊&#xff0c;接口&#xff0c;多态等概念联系在一起感觉更混乱了&#xff0c;所以打算先把最近学的理清一遍&#xff0c;敲一遍代码再往后学...

京东--数据开发实习生--保险业务部门--一面凉经

Base&#xff1a; 本人投递的是后台开发岗位&#xff0c;调剂到数据开发岗位&#xff0c;京东的数据开发也做后台开发方面的工作&#xff0c;还包括算法、策略、数据挖掘和数据平台搭建之类的职责。面试内容基本只会问简历上的&#xff0c;在此基础上再去考察岗位职责相关的内…...

【Hugging Face 开源库】Diffusers 库 —— 扩散模型

Diffusers 的三个主要组件1. DiffusionPipeline&#xff1a;端到端推理工具__call__ 函数callback_on_step_end 管道回调函数 2. 预训练模型架构和模块UNetVAE&#xff08;Variational AutoEncoder&#xff09;图像尺寸与 UNet 和 VAE 的关系EMA&#xff08;Exponential Moving…...