SpringSecurity5(7-会话管理)

news/2025/3/19 10:24:07/文章来源:https://www.cnblogs.com/penggx/p/18771575

会话管理

http.sessionManagement()

  1. invalidSessionUrl(String invalidSessionUrl):指定会话失效时(请求携带无效的 JSESSIONID 访问系统)重定向的 URL,默认重定向到登录页面。
  2. invalidSessionStrategy(InvalidSessionStrategy invalidSessionStrategy):指定会话失效时(请求携带无效的 JSESSIONID 访问系统)的处理策略。
  3. maximumSessions(int maximumSessions):指定每个用户的最大并发会话数量,-1 表示不限数量。
  4. maxSessionsPreventsLogin(boolean maxSessionsPreventsLogin):如果设置为 true,表示某用户达到最大会话并发数后,新会话请求会被拒绝登录;如果设置为 false,表示某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求时失效并根据 expiredUrl() 或者 expiredSessionStrategy() 方法配置的会话失效策略进行处理,默认值为 false。
  5. expiredUrl(String expiredUrl):如果某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求时失效并重定向到 expiredUrl。
  6. expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy):如果某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求中失效并按照该策略处理请求。注意如果本方法与 expiredUrl() 同时使用,优先使用 expiredUrl() 的配置。
  7. sessionRegistry(SessionRegistry sessionRegistry):设置所要使用的 sessionRegistry,默认配置的是 SessionRegistryImpl 实现类
  8. sessionCreationPolicy:控制如何管理 Session
  • SessionCreationPolicy.ALWAYS:总是创建 HttpSession
  • SessionCreationPolicy.IF_REQUIRED:SpringSecurity 只会在需要时创建一个 HttpSession
  • SessionCreationPolicy.NEVER:SpringSecurity 不会创建 HttpSession,但如果他已经存在,将可以使用 HttpSession
  • SessionCreationPolicy.STATELESS:SpringSecurity 永远不会创建 HttpSession,他不会使用 HttpSession 来获取 SecurityContext

invalidSessionUrl

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {/*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().and().authorizeRequests().anyRequest().authenticated();// 开启 Session 会话管理配置http.sessionManagement()// 设置 Session 会话失效时重定向路径,默认为 loginPage().invalidSessionUrl("/login/page");        }
}
# session 失效时间,单位是秒,默认为 30min
server.servlet.session.timeout=30m
# JSESSIONID (Cookie)的生命周期,单位是秒,默认为 -1
server.servlet.session.cookie.max-age=-1

注意:Session 的失效时间至少要 1 分钟,少于 1 分钟按照 1 分钟配置

invalidSessionStrategy

/*** 用户请求携带无效的 JSESSIONID 访问时的处理策略,即对应的 Session 会话失效*/
@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@Overridepublic void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException {// 清除浏览器中的无效的 JSESSIONIDCookie cookie = new Cookie("JSESSIONID", null);cookie.setPath(getCookiePath(request));cookie.setMaxAge(0);response.addCookie(cookie);String xRequestedWith = request.getHeader("x-requested-with");// 判断前端的请求是否为 ajax 请求if ("XMLHttpRequest".equals(xRequestedWith)) {// 响应 JSON 数据response.setContentType("application/json;charset=utf-8");response.getWriter().write("SESSION 失效,请重新登录!");}else {// 重定向到登录页面redirectStrategy.sendRedirect(request, response, "/login/page");}}private String getCookiePath(HttpServletRequest request) {String contextPath = request.getContextPath();return contextPath.length() > 0 ? contextPath : "/";}
}
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {// 自定义 Session 会话失效策略@Autowiredprivate CustomInvalidSessionStrategy invalidSessionStrategy;  /*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().and().authorizeRequests().anyRequest().authenticated();// 开启 Session 会话管理配置http.sessionManagement()// 设置 Session 会话失效时重定向路径,默认为 loginPage()// .invalidSessionUrl("/login/page")// 配置使用自定义的 Session 会话失效处理策略.invalidSessionStrategy(invalidSessionStrategy);       }
}

会话并发控制

会话并发管理是指在当前系统中,同一个用户可以同时创建多少个会话,如果一台设备对应一个会话,也可以理解为同一个用户可以同时在多少个设备上进行登录。

在 Spring Security 中默认情况下,同一个用户在多少个设备上登录并没有限制,但是我们可以自己设置。

会话销毁监听

Spring Security 中通过一个 Map 集合来维护当前 HttpSession 记录,进而实现会话的并发管理,当用户登录成功后,就向集合添加一条 HttpSession 记录。

Map 的 key 是当前用户对象,value 是一个集合,这个集合中保存着这个用户的所有会话 session(这里存储的 session 是包装后的 session),每次登录后就能够去 Map 里面拿出来这个用户的所有会话然后判断一下(set)就知道该不该登录了,当用户注销登录的时候,用户的 session 会被自动销毁,但是 Map 中的 List 集合中的 session 并不会自动移除,所以就导致每次登录的时候都会判断为 session 已经登录,所以我们应当在用户注销登录的时候,将 list 集合中把用户对应的会话 session 移除掉

HttpSessionEventPublisher 实现了 HttpSessionListener 接口,可以监听到 HttpSession 的创建和销毁事件,并将 HttpSession 的创建和销毁事件发布出去,这样当有 HttpSession 销毁时,Spring Security 就可以感知到该事件了

/*** 这个可以监听到 HttpSession 的销毁操作,当有 HttpSession 销毁的时候,就将这个销毁的事件广播出去* @return*/
@Bean
public HttpSessionEventPublisher sessionEventPublisher() {return new HttpSessionEventPublisher();
}

Spring Security 是通过监听 HttpSession 对象的销毁事件来触发会话信息集合 principals 和 sessionIds 的清理工作,但是默认情况下是没有注册过相关的监听器,这会导致 Spring Security 无法正常清理过期或已注销的会话

限制用户二次登录

如果同一个用户在第二个地方登录,则不允许他二次登录

不建议使用该配置,因为用户一旦被盗号,那真正的用户后续就无法登录,只能通过联系管理员解决,所以如果只能一个用户 Session 登录,一般是新会话登录并将老会话踢下线

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {// 自定义 Session 会话失效策略@Autowiredprivate CustomInvalidSessionStrategy invalidSessionStrategy;  /*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().and().authorizeRequests().anyRequest().authenticated();// 开启 Session 会话管理配置http.sessionManagement()// 配置使用自定义的 Session 会话失效处理策略.invalidSessionStrategy(invalidSessionStrategy)// 设置单用户的 Session 最大并发会话数量,-1 表示不受限制.maximumSessions(1)// 设置为 true,表示某用户达到最大会话并发数后,新会话请求会被拒绝登录.maxSessionsPreventsLogin(true);      // 设置所要使用的 sessionRegistry,默认为 SessionRegistryImpl 实现类.sessionRegistry(sessionRegistry());}/*** 注册 SessionRegistry,该 Bean 用于管理 Session 会话并发控制*/@Beanpublic SessionRegistry sessionRegistry() {return new SessionRegistryImpl();}/*** 配置 Session 的监听器(注意:如果使用并发 Sessoion 控制,一般都需要配置该监听器)* 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效的问题*/@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}
}

踢人下线

如果同一个用户在第二个地方登录,则将第一个踢下线

/*** 前提:Session 并发处理的配置为 maxSessionsPreventsLogin(false)* 用户的并发 Session 会话数量达到上限,新会话登录后,最老会话会在下一次请求中失效,并执行此策略*/
@Component
public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@Overridepublic void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {HttpServletRequest request = event.getRequest();HttpServletResponse response = event.getResponse();// 最老会话被踢下线时显示的信息UserDetails userDetails = (UserDetails) event.getSessionInformation().getPrincipal();String msg = String.format("用户[%s]在另外一台机器登录,您已下线!", userDetails.getUsername());String xRequestedWith = event.getRequest().getHeader("x-requested-with");// 判断前端的请求是否为 ajax 请求if ("XMLHttpRequest".equals(xRequestedWith)) {// 认证成功,响应 JSON 数据response.setContentType("application/json;charset=utf-8");response.getWriter().write(msg);}else {// 返回到登录页面显示信息AuthenticationException e = new AuthenticationServiceException(msg);request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", e);redirectStrategy.sendRedirect(request, response, "/login/page?error");}}
}
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {// 自定义 Session 会话失效策略@Autowiredprivate CustomInvalidSessionStrategy invalidSessionStrategy;  // 自定义最老会话失效策略@Autowiredprivate CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy;/*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 开启 Session 会话管理配置http.sessionManagement()// 配置使用自定义的 Session 会话失效处理策略.invalidSessionStrategy(invalidSessionStrategy)// 设置单用户的 Session 最大并发会话数量,-1 表示不受限制.maximumSessions(1)// 设置为 true,表示某用户达到最大会话并发数后,新会话请求会被拒绝登录;// 设置为 false,表示某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求时失效.maxSessionsPreventsLogin(false)// 设置所要使用的 sessionRegistry,默认为 SessionRegistryImpl 实现类.sessionRegistry(sessionRegistry())// 最老会话在下一次请求时失效,并重定向到 /login/page//.expiredUrl("/login/page");// 最老会话在下一次请求时失效,并按照自定义策略处理.expiredSessionStrategy(sessionInformationExpiredStrategy);}/*** 注册 SessionRegistry,该 Bean 用于管理 Session 会话并发控制*/@Beanpublic SessionRegistry sessionRegistry() {return new SessionRegistryImpl();}/*** 配置 Session 的监听器(如果使用并发 Sessoion 控制,一般都需要配置)* 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效问题*/@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}   
}

实现原理

1、AbstractAuthenticationProcessingFilter 的 doFilter()

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();//...public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest)req;HttpServletResponse response = (HttpServletResponse)res;if (!this.requiresAuthentication(request, response)) {//(1) 判断该请求是否为 POST 方式的登录表单提交请求,如果不是则直接放行,进入下一个过滤器chain.doFilter(request, response);} else {if (this.logger.isDebugEnabled()) {this.logger.debug("Request is to process authentication");}// Authentication 是用来存储用户认证信息的类,后续会进行详细介绍Authentication authResult;try {//(2) 调用子类 UsernamePasswordAuthenticationFilter 重写的方法进行身份认证,// 返回的 authResult 对象封装认证后的用户信息authResult = this.attemptAuthentication(request, response);if (authResult == null) {return;}//(3) Session 策略处理(如果配置了用户 Session 最大并发数,就是在此处进行判断并处理)// 默认使用的是新创建的 NullAuthenticatedSessionStrategy 实例,而 UsernamePasswordAuthenticationFilter 过滤器使用的是 CompositeSessionAuthenticationStrategy 实例this.sessionStrategy.onAuthentication(authResult, request, response);} catch (InternalAuthenticationServiceException var8) {this.logger.error("An internal error occurred while trying to authenticate the user.", var8);//(4) 认证失败,调用认证失败的处理器this.unsuccessfulAuthentication(request, response, var8);return;} catch (AuthenticationException var9) {this.unsuccessfulAuthentication(request, response, var9);return;}//(4) 认证成功的处理if (this.continueChainBeforeSuccessfulAuthentication) {// 默认的 continueChainBeforeSuccessfulAuthentication 为 false,所以认证成功之后不进入下一个过滤器chain.doFilter(request, response);}// 调用认证成功的处理器this.successfulAuthentication(request, response, chain, authResult);}}//...public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {this.sessionStrategy = sessionStrategy;}    
}

2、CompositeSessionAuthenticationStrategy 的 onAuthentication()

public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy {//...private final List<SessionAuthenticationStrategy> delegateStrategies;public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {SessionAuthenticationStrategy delegate;// delegateStrategies 是 Session 处理策略集合,会调用这些策略的 onAuthentication() 方法// 包括处理 Session 并发数的策略 ConcurrentSessionControlAuthenticationStrategyfor(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext(); delegate.onAuthentication(authentication, request, response)) {delegate = (SessionAuthenticationStrategy)var4.next();if (this.logger.isDebugEnabled()) {this.logger.debug("Delegating to " + delegate);}}}//...
}

3、ConcurrentSessionControlAuthenticationStrategy 的 onAuthentication()

public class ConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy {//...public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {//(1) 获取用户在系统中的 Session 列表,元素类型为 SessionInformation,该类后续会介绍List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);//(2) 获取用户在系统的并发 Session 数量int sessionCount = sessions.size();//(3) 获取用户能够允许的最大并发 Session 数量int allowedSessions = this.getMaximumSessionsForThisUser(authentication);//(4) 判断当前用户的并发 Session 数量是否达到上限if (sessionCount >= allowedSessions) {// allowedSessions 为 -1,表示并发 Session 数量不受限制if (allowedSessions != -1) {//(5) 当已存在的 Session 数量等于最大并发 Session 数量时if (sessionCount == allowedSessions) HttpSession session = request.getSession(false);if (session != null) {Iterator var8 = sessions.iterator();while(var8.hasNext()) {SessionInformation si = (SessionInformation)var8.next();//(6) 当前验证的会话如果并非新的会话,则不做任何处理if (si.getSessionId().equals(session.getId())) {return;}}}}//(5) 否则,进行策略判断this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);}}}protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {//(1) exceptionIfMaximumExceeded 就是配置类中 maxSessionsPreventsLogin() 方法参数if (!this.exceptionIfMaximumExceeded && sessions != null) {// 当配置 maxSessionsPreventsLogin(false) 时,才运行此处代码//(2) 将用户的 SessionInformation 列表按照最后一次访问时间进行排序sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));//(3) 获取需要踢下线的 SessionInformation 列表(最老会话列表)int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);Iterator var6 = sessionsToBeExpired.iterator();while(var6.hasNext()) {//(4) 将用户最老会话列表中的所有 SessionInformation 对象记为过期// 注意这里只是标记,而不是真正的将 HttpSession 对象过期,// 只有最老会话再次请求或者达到过期时间,HttpSession 对象才会真正失效SessionInformation session = (SessionInformation)var6.next();session.expireNow();}} else {// 当配置 maxSessionsPreventsLogin(true) 时,运行此处代码//(2) 当前(最新)会话的请求访问抛出异常,返回信息(超出最大并发 Session 数量)throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));}}    //...
}

其他

统计未过期的 Session 数量

@Controller
public class TestController {//...@Autowiredprivate SessionRegistry sessionRegistry;//...@GetMapping("/test4")@ResponseBodypublic Object getOnlineSession() {// 统计当前用户未过期的并发 Session 数量UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(user, false);return new ResultData<>(sessions.size());}
}

统计所有在线用户

@Controller
public class TestController {//...@Autowiredprivate SessionRegistry sessionRegistry;//...@GetMapping("/test5")@ResponseBodypublic Object getOnlineUsers() {// 统计所有在线用户List<String> userList = sessionRegistry.getAllPrincipals().stream().map(user -> ((UserDetails) user).getUsername()).collect(Collectors.toList());return new ResultData<>(userList);}    
}

可以使用 httpOnly 和 secure 标签来保护我们的会话 cookie:

  1. httpOnly:如果为 true,那么浏览器脚本将无法访问 cookie
  2. secure:如果为 true,则 cookie 将仅通过 HTTPS 连接发送
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true

会话集群控制

<!-- redis 依赖启动器 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis 数据源 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.8.0</version>
</dependency>
<!-- 使用 Redis 管理 session -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379
# Redis 服务器连接密码(默认无)
spring.redis.password=
# Redis数据库索引(默认为0)
spring.redis.database=1
# 连接池最大连接数(使用负值表示没有限制),默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池大阻塞等待时间(使用负值表示没有限制),默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=10
# 连接池中的小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
# 连接超时时间
spring.redis.timeout=PT10S# 使用 Redis 存储 Session,默认为 none(使用内存存储)
spring.session.store-type=redis
# 指定存储 SessionId 的 Cookie 名(使用 Redis 存储 Session 后,Cookie 名默认会变为 SESSION)
server.servlet.session.cookie.name=JSESSIONID

Redis 存储 Session 默认的序列化方式为 JdkSerializationRedisSerializer,所以存入 Session 的对象都要实现 Serializable 接口

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

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

相关文章

jstat--java内存分析工具

jstat利用JVM内建的指令对应用程序的资源和性能实时的命令行监控,包括堆大小和垃圾回收状况。命令## n1 刷新时间 n2 总共输出次数 jstat -gcutil pid n1 n2说明S0 :s0已使用的容量占比 S1:s1已使用的容量占比 E:eden已使用容量占比 O:老年代已使用容量占比 CCS:压缩类…

AI与.NET技术实操系列(八):使用Catalyst进行自然语言处理

引言 自然语言处理(Natural Language Processing, NLP)是人工智能领域中最具活力和潜力的分支之一。从智能客服到机器翻译,再到语音识别,NLP技术正以其强大的功能改变着我们的生活方式和工作模式。 Catalyst的推出极大降低了NLP技术的应用门槛。它支持文本分类、实体识别等…

ACT中使用正则表达式验证UI展示

近在编写ACT的过程中,遇到校验UI的展示校验,要求数据由Number+空格+base unit进行展示。同事分享了一份参考方法如下图:最终在方法里面使用如下: Public void functionName(String verifyContent) throw exception{String regex = \\d+\\s\\S+;Controller.assertTrue(Pat…

【GreatSQL优化器-17】DYNAMIC RANGE

【GreatSQL优化器-17】DYNAMIC RANGE 一、DYNAMIC RANGE介绍 GreatSQL 的优化器有一种扫描方式是动态范围扫描方式,类似于“已读乱回”模式,这种模式是在表有多个索引的情况下,对驱动表连接的时候部分选择索引的情况。优化器没有找到好的索引可以使用,但发现在知道前面表的…

20244225 《python程序设计》实验一报告

课程:《Python程序设计》 班级: 2442 姓名: 孟雨欣 学号:2024422 实验教师:王志强 实验日期:2024年3月18日 必修/选修: 公选课 一、实验内容 1.熟悉Python开发环境; 本次实验使用pycharm作为开发环境,首先在官网下载并安装PyCharm社区版,安装完成后打开软件,创建一个…

(收藏)UniGUI学习之UniDBGrid常用方法(转载)

1]DataSource设置 2]显示MEMO类型里的文字3]显示悬浮提示4]显示当前记录及总记录数5]读取所有记录,及分页6]在前面加上序号列,并设置序号字体大小,所有单元格 字体大小,垂直居中7]不显示标题栏8]点击表头,单列排序UniDBGrid1ColumnSort9]编辑 和 更新 数据10]获得某单元格里…

纳米压印技术-测序芯片-flowcell-代加工-外协加工-委外加工-激光代加工-河南郑州-芯晨微纳(河南)

纳米压印技术(Nanoimprint Lithography, NIL)是一种高分辨率、低成本的微纳米结构制造技术,通过机械压印方式在材料表面形成纳米级图案。基本原理 核心思想:通过物理压印将模板上的纳米结构“转移”到目标材料(如聚合物、树脂等)上,替代传统光刻中的光学曝光。 关键步骤…

win11关闭实时保护(联想小新版本)

确保没有其他乱七八糟的杀毒软件,不然找不到实时保护 首先关闭联想电脑管家的安全防护 然后打开设置,搜索并进入 病毒和威胁防护 点击“病毒和威胁防护”设置下的管理设置 关闭实时防护即可

2025年最值得尝试的5个AI项目(从入门到高级)

近年来,人工智能已成为一项必备技能,如今,每家公司都希望组建自己的AI团队。这些AI团队的核心目标,是充分利用人工智能所带来的巨大能力。 另一方面,由于公司希望在团队中招聘AI专家,他们寻找的并不仅仅是会使用AI的人,而是能够根据公司的需求,量身定制AI工作流的人才。…

unigui 中 为uniDBGrid设置文字操作栏(转)

unigui中为uniDBGrid设置文字操作栏,如下图的效果,用户点击审核,执行审核代码,点退回,执行退回代码: 对于Web应用界面,这是最常见的方式,那对于我等Delphi开发者来说,基于uniGUI该怎么实现呢? 首先,为uniDBGrid准备“操作”这一栏的显示内容,具体说,在数据集中准…

VSCode 开启 Javascript 内嵌的类型提示

默认情况,VSCode 的代码类型提示需要鼠标放置在函数名或者变量名上才出现,有没有觉得这样比较傻瓜~ VSCode 早在 1.60 版本就引入了内嵌提示配置,每次更新都没仔细看文档,感觉错过了很多的小惊喜啊。 默认的代码显示 你的编辑器代码是不是显示这个样子:开启内嵌的类型提示…

系统高可用的 10 条军规

前言 系统高可用是非常经典的问题,无论在面试,还是实际工作中,都经常会遇到。 这篇文章跟大家一起聊聊,保证系统高可用的10个小技巧,希望对你会有所帮助。1 冗余部署 场景:某电商大促期间,数据库主节点突然宕机,导致全站交易瘫痪。 问题:单节点部署的系统,一旦关键组…