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

深入Spring Security魔幻山谷-获取认证机制核心原理讲解(新版)

文/朱季谦

这是一个古老的传说。

在神秘的Web系统世界里,有一座名为Spring Security的山谷,它高耸入云,蔓延千里,鸟飞不过,兽攀不了。这座山谷只有一条逼仄的道路可通。然而,若要通过这条道路前往另一头的世界,就必须先拿到一块名为token的令牌,只有这样,道路上戍守关口的士兵才会放行。

img

想要获得这块token令牌,必须带着一把有用的userName钥匙和password密码,进入到山谷深处,找到藏匿宝箱的山洞(数据库),若能用钥匙打开其中一个宝箱,就证明这把userName钥匙是有用的。正常情况下,宝箱里会有一块记录各种信息的木牌,包含着钥匙名和密码,其密码只有与你所携带的密码检验一致时,才能继续往前走,得到的通行信息将会在下一个关口处做认证,进而在道路尽头处的JWT魔法屋里获得加密的token令牌。

慢着,既然山谷关口处有士兵戍守,令牌又在山谷当中,在还没有获得令牌的情况下,又怎么能进入呢?

设置关口的军官早已想到这种情况,因此,他特意设置了一条自行命名为“login”的道路,没有令牌的外来人员可从这条道路进入山谷,去寻找传说中的token令牌。这条道路仅仅只能进入到山谷,却无法通过山谷到达另一头的世界,因此,它更像是一条专门为了给外来人员获取token令牌而开辟出来的道路。

img

这一路上会有各种关口被士兵把守检查,只有都一一通过了,才能继续往前走,路上会遇到一位名为ProviderManager的管理员,他管理着所有信息提供者Provider......需找到一位可正确带路的信息提供者Provider,在他的引导下,前往山洞(数据库),成功获取到宝箱,拿到里面记录信息的木牌,这样方能验证所携带的username和password是否正确。若都正确,那么接下来就可将信息进行认证,并前往JWT魔法屋获取token令牌。最后携带着token返回到家乡,让族人都可穿过山谷而进入到web系统,去获取更多珍贵的资源。

这就是整个security的游戏规则原理。

那么,在游戏开始之前,我们先了解下当年戍守山谷的军官是如何设置这道权限关口的......

关口的自定义设置主要有三部分:通过钥匙username获取到宝箱;宝箱里的UserDetails通行信息设置;关口通行过往检查SecurityConfig设置。

1.宝箱里的通行信息:

  1 /**2  * 安全用户模型3  *4  * @author zhujiqian5  * @date 2020/7/30 15:276  */7 public class JwtUserDetails implements UserDetails {8     private static final long serialVersionUID = 1L;9 10     private String username;11     private String password;12     private String salt;13     private Collection<? extends GrantedAuthority> authorities;14 15     JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {16         this.username = username;17         this.password = password;18         this.salt = salt;19         this.authorities = authorities;20     }21 22     @Override23     public String getUsername() {24         return username;25     }26 27     @JsonIgnore28     @Override29     public String getPassword() {30         return password;31     }32 33     public String getSalt() {34         return salt;35     }36 37     @Override38     public Collection<? extends GrantedAuthority> getAuthorities() {39         return authorities;40     }41 42     @JsonIgnore43     @Override44     public boolean isAccountNonExpired() {45         return true;46     }47 48     @JsonIgnore49     @Override50     public boolean isAccountNonLocked() {51         return true;52     }53 54     @JsonIgnore55     @Override56     public boolean isCredentialsNonExpired() {57         return true;58     }59 60     @JsonIgnore61     @Override62     public boolean isEnabled() {63         return true;64     }65 66 }

这里JwtUserDetails实现Spring Security 里的UserDetails类,这个类是长这样的,下面对各个字段做了注释:

  1 public interface UserDetails extends Serializable {2 	/**3 	*用户权限集,默认需要添加ROLE_前缀4 	*/5 	Collection<? extends GrantedAuthority> getAuthorities();6 7 	/**8 	*用户的加密密码,不加密会使用{noop}前缀9 	*/10 	String getPassword();11 12 	/**13 	*获取应用里唯一用户名14 	*/15 	String getUsername();16 17 	/**18 	*检查账户是否过期19 	*/20 	boolean isAccountNonExpired();21 22 	/**23 	*检查账户是否锁定24 	*/25 	boolean isAccountNonLocked();26 27 	/**28 	*检查凭证是否过期29 	*/30 	boolean isCredentialsNonExpired();31 32 	/**33 	*检查账户是否可用34 	*/35 	boolean isEnabled();36 }

说明:JwtUserDetails自定义实现了UserDetails类,增加username和password字段,除此之外,还可以扩展存储更多用户信息,例如,身份证,手机号,邮箱等等。其作用在于可构建成一个用户安全模型,用于装载从数据库查询出来的用户及权限信息。

2.通过钥匙username获取到宝箱方法:

  1 /**2  * 用户登录认证信息查询3  *4  * @author zhujiqian5  * @date 2020/7/30 15:306  */7 @Service8 public class UserDetailsServiceImpl implements UserDetailsService {9 10     @Resource11     private SysUserService sysUserService;12 13     @Override14     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {15         SysUser user = sysUserService.findByName(username);16         if (user == null) {17             throw new UsernameNotFoundException("该用户不存在");18         }19 20         Set<String> permissions = sysUserService.findPermissions(user.getName());21         List<GrantedAuthority> grantedAuthorities = permissions.stream().map(AuthorityImpl::new).collect(Collectors.toList());22         return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);23     }24 }

这个自定义的UserDetailsServiceImpl类实现了Spring Security框架自带的UserDetailsService接口,这个接口只定义一个简单的loadUserByUsername方法:

  1 public interface UserDetailsService {2 	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;3 }

根据loadUserByUsername方法名便能看出,这是一个可根据username用户名获取到User对象信息的方法,并返回一个UserDetails对象,即前头的“宝箱里的通行信息”,换言之,通过重写这个方法,我们能在该方法里实现用户登录认证信息的查询,并返回对应查询信息。

综合以上代码,先用开头提到的宝箱意象做一个总结,即拿着userName这把钥匙,通过loadUserByUsername这个方法指引,可进入到山洞(数据库),去寻找能打开的宝箱(在数据库里select查询userName对应数据),若能打开其中一个宝箱(即数据库里存在userName对应的数据),则获取宝箱里的通行信息(实现UserDetails的JwtUserDetails对象信息)。

3.关口通行过往检查设置

自定义的SecurityConfig配置类是SpringBoot整合Spring Security的关键灵魂所在。该配置信息会在springboot启动时进行加载。其中,authenticationManager() 会创建一个可用于传token做认证的AuthenticationManager对象,而AuthenticationManagerBuilder中的auth.authenticationProvider()则会创建一个provider提供者,并将userDetailsService注入进去,该userDetailsService的子类被自定义的UserDetailsServiceImpl类继承,并重写loadUserByUsername()方法,因此,当源码里执行userDetailsService的loadUserByUsername()方法时,即会执行被重写的子类loadUserByUsername()方法。

由此可见,在做认证的过程中,只需找到注入userDetailsService的provider对象,即可执行loadUserByUsername去根据username获取数据库里信息。

那具体是在哪个provider对象?请看下面详细解析。

  1 @Configuration2 @EnableWebSecurity3 @EnableGlobalMethodSecurity(prePostEnabled = true)4 public class SecurityConfig extends WebSecurityConfigurerAdapter {5 6     @Resource7     private UserDetailsService userDetailsService;8 9     @Override10     public void configure(AuthenticationManagerBuilder auth) {11      auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));12     }13 14     @Bean15     @Override16     public AuthenticationManager authenticationManager() throws Exception {17         return super.authenticationManager();18     }19 20     @Override21     protected void configure(HttpSecurity httpSecurity) throws Exception {22         //使用的是JWT,禁用csrf23         httpSecurity.cors().and().csrf().disable()24                 //设置请求必须进行权限认证25                 .authorizeRequests()26                 //跨域预检请求27                 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()28                 //permitAll()表示所有用户可认证29                 .antMatchers( "/webjars/**").permitAll()30                 //首页和登录页面31                 .antMatchers("/").permitAll()32                 .antMatchers("/login").permitAll()33                 // 验证码34                 .antMatchers("/captcha.jpg**").permitAll()35                 // 其他所有请求需要身份认证36                 .anyRequest().authenticated();37         //退出登录处理38         httpSecurity.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());39         //token验证过滤器40         httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);41     }42 }

首先,双击SecurityConfig 类里的JwtAuthenticationProvider——

img

进入到JWTAuthenticationProvider类内部,发现原来该类是继承了DaoAuthenticationProvider。

img

请注意这段话,很关键:

点击setUserDetailsService(userDetailsService)。进入到方法里面后,发现这里其实是把UserDetailsService通过set方式依赖注入到DaoAuthenticationProvider类中,换言之,我们接下来在加载完成的框架里只需通过DaoAuthenticationProvider的getUserDetailsService()方法,便可获取前面注入的userDetailsService,进而调用其子类实现的loadUserByUsername()方法。

看到这里,您须重点关注一下DaoAuthenticationProvider这个类,它将会在后面再次与我们碰面,而它是一个AuthenticationProvider。

若您还不是很明白AuthenticationProvider究竟是什么,那就暂且统一把它当做信息提供者吧,而它是ProviderManager管理员底下其中一个信息提供者Provider。

img

写到这里,还有一个疑问,即security框架是如何将信息提供者Provider归纳到ProviderManager管理员手下的呢?

解答这个问题,需回到SecurityConfig配置文件里,点击authenticationProvider进入到底层方法当中。

img

进入后,里面是具体的方法实现,大概功能就是把注入了userDetailsService的信息提供者DaoAuthenticationProvider添加到一个List集合里,然后再将集合里的所有提供者,通过构造器传入ProviderManager,命名生成一个新的提供者管理员providerManager。这里面还涵盖不少细节,感兴趣的读者可自行再扩展深入研究。

img

以上,就初步设置好了游戏规则。

接下来,就是主角上场了。

在所有的游戏里,都会有一个主角,而我们这个故事,自然也不例外。

img

此时,在一扇刻着“登录”二字的大门前,有一个小兵正在收拾他的包袱,准备跨过大门,踏上通往Spring Security山谷的道路。他背负着整个家族赋予的任务,需前往Security山谷,拿到token令牌,只有把它成功带回来,家族里的其他成员,才能有机会穿过这座山谷,前往另一头的神秘世界,获取到珍贵的资源。

这个小兵,便是我们这故事里的主角,我把他叫做线程,他将带着整个线程家族的希望,寻找可通往神秘系统世界的令牌。

线程把族长给予的钥匙和密码放进包袱,他回头看了一眼自己的家乡,然后挥了挥手,跨过“登录”这扇大门,勇敢地上路了。

线程来到戒备森严的security关口前,四周望了一眼,忽然发现关口旁立着一块显眼的石碑,上面刻着一些符号。他走上前一看,发现原来是当年军官设置的指令与对应的说明:

  1     @Override2     protected void configure(HttpSecurity httpSecurity) throws Exception {3         //使用的是JWT,禁用csrf4         httpSecurity.cors().and().csrf().disable()5                 //设置请求必须进行权限认证6                 .authorizeRequests()7                 //跨域预检请求8                 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()9                 //首页和登录页面10                 .antMatchers("/login").permitAll()11                 // 其他所有请求需要身份认证12                 .anyRequest().authenticated();13         //退出登录处理14         httpSecurity.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());15         //token验证过滤器16         httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);17     }

其中,permitAll()代表所有请求都可访问,当它设置成类似“.antMatchers("/login").permitAll()”的形式时,则代表该/login路径请求无需认证便可通过,相反,代码anyRequest().authenticated()则意味着其他的所有请求都必须进行身份验证方能通过,否则,会被拒绝访问。

下面,将通过debug一步一步揭示,线程是如何闯关升级的,最后成功获取到传说中的token令牌。

线程来到关口处,不久,在戍守士兵的指引下,开始往login道路走去,前面迎接他,将是一系列的关口检查。

1.传入userName,password属性,封装成一个token对象。

img

进入到该对象里,可看到用户名赋值给this.principal,密码赋值给this.credentials,其中setAuthenticated(false)意味着尚未进行认证。

img

注意一点是,UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken,而AbstractAuthenticationToken实现Authentication,由传递关系可知,Authentication是UsernamePasswordAuthenticationToken的基类,故而UsernamePasswordAuthenticationToken是可以向上转换为Authentication,理解这一点,就能明白,为何接下来authenticationManager.authenticate(token)方法传进去的是UsernamePasswordAuthenticationToken,但在源码里,方法参数则为Authentication。

2.将username,password封装成token对象后,通过Authentication authentication=authenticationManager.authenticate(token)方法进行认证,里面会执行一系列认证操作,需要看懂源码,才能知道这行代码背后藏着的水月洞天,然,有一点是可以从表面上看懂的,即若成功认证通过,将会返回一个认证成功的Authentication对象,至于对象里是什么信息,请继续 往下看。

img

3.点击进入到AuthenticationManager里,发现该接口里只有一个方法:

  1 Authentication authenticate(Authentication authentication)2 			throws AuthenticationException;

由此可知,它的具体实现,是通过实现类来操作的,它的主要实现类有N多个,其中,在认证过程中,我们需关注的是ProviderManager这个类。

img

这个ProviderManager,即前面提到的Provider管理员,他管理着一堆信息提供者provider。线程此行的目的,就是先找到这个Provider管理员,再去管理员手中寻找能够匹配到的提供者provider,只有通过匹配到的提供者,才能找到获取数据库的方法loadUserByUsername。

4.ProviderManager类实际上是实现AuthenticationManager接口,重写了authenticate方法。因此,当前面代码执行authenticationManager.authenticate(token)方法时,具体实现将由其子类重写的方法操作,子类即ProviderManager。

img

debug进去后——

img

继续往下执行,通过getProviders() 可获取到内部维护在List中的AuthenticationProvider遍历进行验证,若该提供者能支持传入的token进行验证,则继续往下执行。

img

其中,JwtAuthAuthenticationProvider可执行本次验证,而JwtAuthAuthenticationProvider是继承DaoAuthenticationProvider后自定义的类,可以理解成,进行认证验证的Provider是前面重点提到的DaoAuthenticationProvider。

img

DaoAuthenticationProvider是一个具体实现类,它继承AbstractUserDetailsAuthenticationProvider抽象类。

img

而AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口。

img

5.在ProviderManager中,执行到result = provider.authenticate(authentication)时,其中provider是由AuthenticationProvider定义的,但AuthenticationProvider是一个接口,需由其子类具体实现。根据上面分析,可知,AbstractUserDetailsAuthenticationProvider会具体实现provider.authenticate(authentication)方法。debug进入到其authenticate方法当中,会跳转到AbstractUserDetailsAuthenticationProvider重写的authenticate()方法当中,接下来会详细介绍该authenticate()执行的代码模块:

img

5.1.首先,第一步,会执行this.userCache.getUserFromCache(username)获取缓存里的信息。

img

5.2 若缓存里没有UserDetails信息,将会继续往下执行,执行到retrieveUser方法,该方法的总体作用是:通过登录时传入的userName去数据库里做查询,若查询成功,便将数据库的User信息包装成UserDetails对象返回,当然,具体如何从数据库里获取到信息,则需要重写一个方法,即前面提到的loadUserByUsername()方法。

值得注意一点是,一般新手接触到security框架,都会有一个疑问,即我登录时传入了username,是如何获取到数据库里的用户信息?

其实,这个疑问的关键答案,就藏在这个retrieveUser()方法里。该方法名的英文解析是:“(训练成能寻回猎物的)猎犬”。我觉得这个翻译在这里很有意思,暂且可以把它当成信息提供者Provider驯养的一头猎犬,它可以帮我们的游戏主角线程在茫茫的森林里,寻找到藏匿宝箱的山洞(数据库)。

img

5.3 ,接下来,就让这头猎犬给我们带路吧——点击retrieveUser(),进入到方法当中,发现,这其实是一个抽象方法,故而其具体实现将在子类中进行。

img

5.4 进入到其子类实现的方法当中,发现会进入前面提到AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider,它也是一个AuthenticationProvider,即所谓的信息提供者之一。在DaoAuthenticationProvider类里,实现了父类的retrieveUser方法。

在猎犬的(retrieveUser)的带路下,我们最后看到 了熟悉的老朋友,关键方法loadUserByUserName()。

img

点进loadUserByUsername()方法里,会进入到UserDetailsService接口里,该接口只有loadUserByUsername一个方法,该方法具体在子类里实现。

img

这个接口被我们自定义重写了,即前面露过面的:

img

在DaoAuthenticationProvider类中,调用loadUserByUserName()方法时,最终会执行我们重写的loadUserByUsername()方法,该方法将会去数据库里查询username的信息,并返回一个User对象,最后SysUser对象转换成UserDetails,返回给DaoAuthenticationProvider对象里的UserDetails,跳转如下图:

img

5.5 DaoAuthenticationProvider的retirieveUser执行完后,会将数据库查询到的UserDetails返回给上一层,即AbstractUserDetailsAuthenticationProvider执行的retrieveUser()方法,得到的UserDetails赋值给user。

img

6.接下来就是各种检查,其中,有一个检查方法需要特别关注,即

img

注:additionalAuthenticationChecks()方法的作用是检查密码是否一致的,前面已根据username去数据库里查询出user数据,接下来,就需要在该方法里,检查数据库里user的密码与登录时传入的密码是否一致了。

6.1 点击additionalAuthenticationChecks()进入到方法里,发现AbstractUserDetailsAuthenticationProvider当中的additionalAuthenticationChecks同样是一个抽象方法,没有具体实现,它与前面的retrieveUser()方法一样,具体实现都在AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider中重写了。

img

6.2.跳转进入子类重写的additionalAuthenticationChecks()当中,先通过authentication.getCredentials().toString()从token对象中获取登录时输入的密码,再通过passwordEncoder.matches(presentedPassword, userDetails.getPassword())进行比较,即拿登录的密码与数据库里取出的密码做对比,执行到这一步,若两个密码一致时,即登录的username和password能与数据库里某个username和密码匹配,则可登录成功。

img

7.用户名与密码都验证通过后,可继续执行下一步操作,中间还有几个检查方法,读者若感兴趣,可自行研究。最后会把user赋值给一个principalToReturn对象,然后连同authentication还有user,一块传入到createSuccessAuthentication方法当中。

img

8.在createSuccessAuthentication方法里,会创建一个已经认证通过的token。

img

点进该token对象当中,可以看到,这次的setAuthenticated设置成了true,即意味着已经认证通过。

img

最后,将生成一个新的token,并以Authentication对象形式返回到最开始的地方。

img

执行到这一步,就可以把认证通过的信息进行存储,到这里,就完成了核心的认证部分。

接下来,我们的主角线程就可以前往JWT魔法屋获取加密的token令牌,然后携带令牌返回故土,届时,其线程家族里的其他成员,都可穿过这座Spring Security山谷,前往山谷另一边的web系统世界了。

img

那是另外一个世界的故事,我们将在以后漫长的岁月当中,缓缓道来.....

而这个关于Spring Security山谷的故事,就暂且记到这里,若当中有不当之处,还需各位大佬指出而加以改进。

img

本文完,插图皆来自网络。

相关文章:

深入Spring Security魔幻山谷-获取认证机制核心原理讲解(新版)

文/朱季谦 这是一个古老的传说。 在神秘的Web系统世界里&#xff0c;有一座名为Spring Security的山谷&#xff0c;它高耸入云&#xff0c;蔓延千里&#xff0c;鸟飞不过&#xff0c;兽攀不了。这座山谷只有一条逼仄的道路可通。然而&#xff0c;若要通过这条道路前往另一头的…...

【知网稳定检索】第九届社会科学与经济发展国际学术会议 (ICSSED 2024)

第九届社会科学与经济发展国际学术会议 (ICSSED 2024) 2024 9th International Conference on Social Sciences and Economic Development 第九届社会科学与经济发展国际学术会议(ICSSED 2024)定于2024年3月22-24日在中国北京隆重举行。会议主要围绕社会科学与经济发展等研究…...

使用Spark写入数据到数据库表

项目场景&#xff1a; 使用Spark写入数据到数据库表 问题描述 Column "20231201" not found in schema Some(StructType(StructField(sdate,IntegerType,false),StructField(date_time,StringType,true),StructField(num,LongType,false),StructField(table_code,S…...

Codebeamer—软件全生命周期管理轻量级平台

产品概述 Codebeamer涵盖了软件研发的生命周期&#xff0c;在一个整合的平台内支持需求管理、测试管理、软件开发过程管理以及项目管理等&#xff0c;同时具有IToperations&DevOps相关的内容&#xff0c;并支持变体管理的功能。对于使用集成的应用程序生命周期管理&#xf…...

Yocto - bb脚本中使用的SRC_URI、SRCREV和S

我们遇到的各种自己不了解的技术或产品时&#xff0c;都需要阅读用户手册。用户手册里的内容很多时&#xff0c;除了由目录组织文档结构外&#xff0c;通常还有有一个词汇表&#xff0c;一般作为附录放在文档最后。 通过这个按照字母排序的词汇表&#xff0c;可以在对整个文档还…...

LeetCode | 965. 单值二叉树

LeetCode | 965. 单值二叉树 OJ链接 首先判断树为不为空&#xff0c;为空直接true然后判断左子树的val&#xff0c;和根的val相不相同再判断右子树的val&#xff0c;和根的val相不相同最后递归左子树和右子树 bool isUnivalTree(struct TreeNode* root) {if(root NULL)retur…...

YOLOv8创新魔改教程(一)如何进行模块创新

YOLOv8创新魔改教程&#xff08;一&#xff09;如何进行模块创新 YOLOv8创新魔改教程 本人研一&#xff0c;最近好多朋友问我要如何修改模型创新模块&#xff0c;就想着不如直接开个专栏歇一歇文章&#xff0c;也算是对自己学习的总结&#xff0c;本专栏以YOLOv8为例&#xf…...

postgresql-shared_buffers参数详解

shared_buffers 是 PostgreSQL 中一个非常关键的参数&#xff0c;用于配置服务器使用的共享内存缓冲区的大小。这些缓冲区用于存储数据页&#xff0c;以便数据库可以更快地访问磁盘上的数据。 这个参数在 PostgreSQL 的性能方面有着重要的影响。增加 shared_buffers 可以提高数…...

windows10 Arcgis pro3.0-3.1

我先安装的arcgis pro3.0&#xff0c;然后下载的3.1。 3.0里面有pro、help、sdk、还有一些补丁包根据个人情况安装。 3.1里面也是这些。 下载 正版试用最新的 ArcGIS Pro 21 天教程&#xff0c;仅需五步&#xff01;-地理信息云 (giscloud.com.cn) 1、安装windowsdesktop-…...

Apache Airflow (十四) :Airflow分布式集群搭建及测试

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…...

解决VSCode按住Ctrl(or Command) 点击鼠标左键不跳转的问题(不能Go to Definition)

问题出现 往往在升级了VSCode以后&#xff0c;就会出现按住Ctrl&#xff08;or Command&#xff09; 点击鼠标左键不跳转的问题&#xff0c;这个问题很常见。 解决办法 1 进入VScode的首选项&#xff0c;选择设置 2 输入Go to definition&#xff0c;找到如下两个设置&#…...

使用DrlParser 检测drl文件是否有错误

为避免运行时候错误&#xff0c;drools 7 可以使用DrlParser预先检测 drl文件是否正常。 parser 过程通常不会返回异常ruleDescr parser.parse(resource); 为空代表有异常 具体测试代码如下&#xff1a; public class DrlParserTest {public static void main(String[] arg…...

ArcGIS中基于人口数据计算人口密度的方法

文章目录 一、密度分析原理二、点密度分析三、线密度分析四、核密度分析一、密度分析原理 密度分析是指根据输入的要素数据集计算整个区域的数据聚集状况,从而产生一个联系的密度表面。通过密度计算,将每个采样点的值散步到整个研究区域,并获得输出栅格中每个像元的密度值。…...

在CentOS 8.2中安装Percona Xtrabackup 8.0.x备份MySql

添加Percona软件库&#xff1a; yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm 安装Percona Xtrabackup 8.0.x&#xff1a; yum install percona-xtrabackup-80 确认安装完成后&#xff0c;您可以使用以下命令验证Percona Xtrabackup的安装…...

javascript中的正则表达式的相关知识积累

01-javascript中的正则表达式用符号/作为正则表达式的开始符和结束符 javascript中的正则表达式用符号/作为正则表达式的开始符和结束符。 即javascript的正则表达式如下所示&#xff1a; /正则表达式/02-^&#xff1a;匹配字符串的开始 ^: 该符号表示匹配字符串的开始。这个…...

51k+ Star!动画图解、一键运行的数据结构与算法教程!

大家好&#xff0c;我是 Java陈序员。 我们都知道&#xff0c;《数据结构与算法》 —— 是程序员的必修课。 无论是使用什么编程语音&#xff0c;亦或者是前后端开发&#xff0c;都需要修好《数据结构与算法》这门课&#xff01; 在各个互联网大产的面试中&#xff0c;对数据…...

4.7 矩阵的转置运算(C语言实现)

【题目描述】用键盘从终端输入一个3行4列的矩阵&#xff0c;编写一个函数对该矩阵进行转置操作。 【题目分析】矩阵的转置运算是线性代数中的一个基本运算。显然&#xff0c;一个m行n列的矩阵经过转置运算后就变成了一个n行m列的矩阵。这个问题的解决关键是要解决两个问题&…...

快速掌握Pyqt5的9种显示控件

Pyqt5相关文章: 快速掌握Pyqt5的三种主窗口 快速掌握Pyqt5的2种弹簧 快速掌握Pyqt5的5种布局 快速弄懂Pyqt5的5种项目视图&#xff08;Item View&#xff09; 快速弄懂Pyqt5的4种项目部件&#xff08;Item Widget&#xff09; 快速掌握Pyqt5的6种按钮 快速掌握Pyqt5的10种容器&…...

【WP】Geek Challenge 2023 web 部分wp

EzHttp http协议基础题 unsign 简单反序列化题 n00b_Upload 很简单的文件上传&#xff0c;上传1.php&#xff0c;抓包&#xff0c;发现php内容被过滤了&#xff0c;改为<? eval($_POST[‘a’]);?>&#xff0c;上传成功&#xff0c;命令执行读取就好了 easy_php …...

Elasticsearch:为现代搜索工作流程和生成式人工智能应用程序铺平道路

作者&#xff1a;Matt Riley Elastic 的创新投资支持开放的生态系统和更简单的开发者体验。 在本博客中&#xff0c;我们希望分享 Elastic 为简化你构建 AI 应用程序的体验而进行的投资。 我们知道&#xff0c;开发人员必须在当今快速发展的人工智能环境中保持灵活性。 然而&a…...

【WinForm.NET开发】Windows窗体开发概述

本文内容 介绍为什么要从 .NET Framework 迁移生成丰富的交互式用户界面显示和操纵数据将应用部署到客户端计算机 Windows 窗体是一个可创建适用于 Windows 的丰富桌面客户端应用的 UI 框架。 Windows 窗体开发平台支持广泛的应用开发功能&#xff0c;包括控件、图形、数据绑…...

WPF 简单绘制矩形

Canvas 画矩形&#xff1a; view和viewModel 绑定一起才显示移动轨迹&#xff08;可以定义一个string 看是否绑定属性的路径是正确的&#xff09; 前台&#xff08;绑定事件和显示移动的线&#xff09;&#xff1a; <Canvas Name"canvas" Background"#01FF…...

crui_lvgl 一个LVGL的DSL辅助工具的设想

设想 Target以LVGL为目标&#xff0c;语法以CSS为Reference。 CSS 规范 略 CSS规范最强大的属于CSS自身的属性很多&#xff0c;可以通过class和伪属性选择器对UI进行直接控制。 QML规范 ApplicationWindow {visible: truewidth: Constants.widthheight: Constants.height…...

公共部门生成式人工智能的未来

作者&#xff1a;Dave Erickson 最近&#xff0c;我与 IDC Government Insights 研究副总裁阿德莱德奥布莱恩 (Adelaide O’Brien) 坐下来讨论了全球公共部门生成式人工智能的当前和未来状况。 完整的对话可以按需查看&#xff0c;但我也想强调讨论中的一些要点。 我们的目标是…...

【报名】2023产业区块链生态日暨 FISCO BCOS 开源六周年生态大会

作为2023深圳国际金融科技节系列活动之一&#xff0c;由深圳市地方金融监督管理局指导&#xff0c;微众银行、金链盟主办的“2023产业区块链生态日暨FISCO BCOS开源六周年生态大会”将于12月15日下午14:00在深圳举办。 今年的盛会将进一步升级&#xff0c;以“FISCO BCOS和TA的…...

MySQL之性能分析和系统调优

MySQL之性能分析和系统调优 性能分析 查看执行计划 EXPLAIN EXPLAIN作为MySQL的性能分析神器&#xff0c;可以用来分析SQL执行计划&#xff0c;需要理解分析结果可以帮助我们优化SQL explain select … from … [where ...]TABLE 表名 查询的每一行记录都对于着一张表 id 该…...

时间复杂度为 O(n^2) 的排序算法 | 京东物流技术团队

对于小规模数据&#xff0c;我们可以选用时间复杂度为 O(n2) 的排序算法。因为时间复杂度并不代表实际代码的执行时间&#xff0c;它省去了低阶、系数和常数&#xff0c;仅代表的增长趋势&#xff0c;所以在小规模数据情况下&#xff0c; O(n2) 的排序算法可能会比 O(nlogn) 的…...

关于前端学习的思考-内边距、边框和外边距

从最简单的盒子开始思考 先把实际应用摆出来&#xff1a; margin&#xff1a;居中&#xff0c;控制边距。 padding&#xff1a;控制边距。 border&#xff1a;制作三角形。 盒子分为内容盒子&#xff0c;内边距盒子&#xff0c;边框和外边距。 如果想让块级元素居中&#…...

【linux】/etc/security/limits.conf配置文件详解、为什么限制、常见限制查看操作

文章目录 一. limits.conf常见配置项详解二. 文件描述符&#xff08;file descriptor&#xff09;简述三. 为什么限制四. 相关操作1. 展示当前资源限制2. 查看系统当前打开的文件描述符数量3. 查看某个进程打开的文件描述符数量4. 各进程占用的文件描述符 /etc/security/limits…...

Windows系统下更新后自带的画图软件出现马赛克bug

一.bug的样子&#x1f357; 在使用橡皮后&#xff0c;原来写的内容会变成马赛克。而我们希望它是纯白色的。 二.解决方法&#x1f357; 第一步 第二步 第三步 三. 解决后的效果&#x1f357; 用橡皮擦随便擦都不会出现马赛克了。 更新过后&#xff0c;想用win自带的画图软件会出…...