深入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

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

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

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

相关文章

Vue + Element ui 实现动态表单,包括新增行/删除行/动态表单验证/提交功能

原创/朱季谦 最近通过Vue Element ui实现了动态表单功能&#xff0c;该功能还包括了动态表单新增行、删除行、动态表单验证、动态表单提交功能&#xff0c;趁热打铁&#xff0c;将开发心得记录下来&#xff0c;方便以后再遇到类似功能时&#xff0c;直接拿来应用。 简化的页…

zabbix6.4.0配置邮件及企微机器人群聊告警

一、邮件告警 根据公司邮箱自行配置&#xff0c;电子邮件、用户账号密码填自己的邮箱账号密码 动作本次使用的默认的&#xff0c;如果为了更加美观可自行修改。 二、企业微信机器人告警 首先在企微上创建群聊&#xff0c;之后添加群聊机器人 将地址复制&#xff0c;后面用 …

MATLAB 模型参考自适应控制 - Model Reference Adaptive Control

系列文章目录 文章目录 系列文章目录前言一、参考模型二、扰动与不确定性模型三、直接 MRAC名义模型参数更新间接 MRAC估计器模型和控制器增益参数更新学习修正参考文献 前言 模型参考自适应控制模块计算控制动作&#xff0c;使不确定的受控系统跟踪给定参考被控对象模型的行为…

【LeetCode刷题笔记】102. 二叉树的层序遍历

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多算法知识专栏&#xff1a;算法分析&#x1f525; 给大家跳段街舞感谢…

kkFileView 从源码编译最新安装包

目录 一、前言二、拉取 kkFileView 最新代码三、kkFileView 打包 一、前言 kkFileView 是一个开源的附件在线预览项目&#xff0c;可以让你的项目方便的在线预览附件&#xff0c;包括比如&#xff1a;doc、docx、pdf、xml、xls、xlsx、ppt、pptx、zip、png、jpg、txt、mp4等常…

测评补单助力亚马逊,速卖通,国际站卖家抢占市场,提升转化和评分

想要快速提升商品的销量&#xff0c;测评补单这种方法见效是最快的。特别是新品上线&#xff0c;缺少用户评价&#xff0c;转化率不好&#xff0c;很多商家新品上线都会做测评补单&#xff0c;搞些商品好评&#xff0c;不但可以提升转化&#xff0c;同时在平台也可以获得更多展…

基于若依的ruoyi-nbcio流程管理系统仿钉钉流程初步完成转bpmn设计(还有bug,以后再修改)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 今天初步完成仿钉钉流程转bpmn设计的工作&#xff0c;当然还有不少bug&#xff0c;以后有需要或者网友也帮…

:deep(){} 样式穿透不生效问题解决方案

在我们写vue的项目的时候&#xff0c;可能会遇到这种情况&#xff0c;父子组件&#xff0c;在父组件中使用了样式穿透给子组件的类添加样式&#xff0c;结果失败了。比如下面这种情况。 父组件 <template><p>我是父组件</p><Son /> </template>…

web前端之css变量的妙用、通过JavaScrip改变css文件中的属性值、querySelector、setProperty

MENU 效果图htmlJavaScripstylequerySelectorsetProperty 效果图 html <div id"idBox" class"p_r w_680 h_160 b_1s_red"><div id"idItem" class"p_a l_0 t_30 w_100 h_100 bc_rgba_255_00_05 radius_50_"></div> …

uniapp实现文件预览过程

H5实现预览 <template><iframe :src"_url" style"width:100vw; height: 100vh;" frameborder"0"></iframe> </template> <script lang"ts"> export default {data() {return {_url: ,}},onLoad(option…

2023年第十二届数学建模国际赛小美赛A题太阳黑子预测求解分析

2023年第十二届数学建模国际赛小美赛 A题 太阳黑子预测 原题再现&#xff1a; 太阳黑子是太阳光球上的一种现象&#xff0c;表现为比周围区域暗的暂时斑点。它们是由抑制对流的磁通量浓度引起的表面温度降低区域。太阳黑子出现在活跃区域内&#xff0c;通常成对出现&#xff…

Vmware安装Centos7

CentOs7镜像文件下载 centos7 镜像文件下载-CSDN博客 配置虚拟机 打开Vmware&#xff0c;点击新建虚拟机 典型安装与自定义安装 典型安装&#xff1a;VMware会将主流的配置应用在虚拟机的操作系统上&#xff0c;对于新手来很友好。 自定义安装&#xff1a;自定义安装可以针…

LangChain(0.0.340)官方文档三:Prompts上——自定义提示模板、使用实时特征或少量示例创建提示模板

文章目录 一、 Prompt templates1.1 langchain_core.prompts1.2 PromptTemplate1.2.1 简介1.2.2 ICEL1.2.3 Validate template 1.3 ChatPromptTemplate1.3.1 使用role创建1.3.2 使用MessagePromptTemplate创建1.3.3 自定义MessagePromptTemplate1.3.3.1 自定义消息角色名1.3.3.…

密码学学习笔记(二十二):RSA签名方案

在RSA中&#xff0c;计算公钥的欧拉函数和私钥是关键步骤。 如何计算呢&#xff1f; RSA算法中的是两个质数 p 和 q 的乘积。所以两个质数必须要找到。一旦找到 p 和 q就可以使用公式() (p-1) (q-1)来计算。 计算私钥d 私钥 d 是满足 e*d ≡ 1 mod   的整数。换句话说&a…

寻找链表的中间节点

问题描述&#xff1a; 给定一个头结点为head的非空单链表&#xff0c;返回链表的中间结点。如果有两个中间节点&#xff0c;则返回第二个中间结点&#xff0c;个数为奇数返回一个值&#xff0c;个数为偶数返回两个中间结点的第二个值。 思路&#xff1a; 用快慢指针来寻找中间…

Android File Transfer for Mac:畅享强大的安卓文件传输工具

作为一款功能强大的安卓文件传输工具&#xff0c;Android File Transfer for Mac&#xff08;以下简称AFT&#xff09;为Mac用户提供了便捷快速的安卓设备文件管理体验。无论是传输照片、音乐、视频还是文档&#xff0c;AFT都能满足你的需求&#xff0c;让你的文件传输更加高效…

设计模式---第三篇

系列文章目录 文章目录 系列文章目录前言一、模板方法模式二、知道享元模式吗?三、享元模式和单例模式的区别?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一…

Centos7安装

想学Vmware安装可以看下下面链接,不想就算了 https://blog.csdn.net/weixin_43895362/article/details/134723073 选择第一项&#xff0c;安装直接CentOS 7&#xff0c;回车 稍等后出现进入下图,选择中文,这个只是安装时的语言 首先设置时间,时区选择上海&#xff0c;查看时…

用 LangChain 搭建基于 Notion 文档的 RAG 应用

如何通过语言模型查询 Notion 文档&#xff1f;LangChain 和 Milvus 缺一不可。 在整个过程中&#xff0c;我们会将 LangChain 作为框架&#xff0c;Milvus 作为相似性搜索引擎&#xff0c;用二者搭建一个基本的检索增强生成&#xff08;RAG&#xff09;应用。在之前的文章中&a…

图解「差分」入门(“前缀和“ 到 “差分“ 丝滑过渡)

题目描述 这是 LeetCode 上的 「1094. 拼车」 &#xff0c;难度为 「中等」。 Tag : 「差分」、「前缀和」 车上最初有 capacity 个空座位&#xff0c;车只能向一个方向行驶&#xff08;不允许掉头或改变方向&#xff09;。 给定整数 capacity 和一个数组 trips, 表示第 i 次旅…