草庐IT

详解Spring Security

_BugMan 2024-02-26 原文

目录

1.概述

2.登录

2.1.默认用户

2.2.自定义用户

2.3.加密

2.4.绕过加密

2.5.怎么传递用户信息

2.6.记住我

3.登出

4.使用数据库

4.1.jdbcAuthentication

4.2.userDetailsService

5.自定义处理器

6.更多细粒度的控制

7.原理简述


1.概述

Spring Security是一个基于Spring框架的安全性框架,它提供了一系列的API和扩展点,可以帮助开发人员在应用程序中轻松地实现安全认证和授权控制。

我们可以理解为Spring Security维护了一组我们可以自定义的访问规则,每次访问都会去进行规则比对,满足规则的才放行。

这些规则可以有很多维度,本文会以最基础的基于角色的控制入手逐步扩展详细介绍Spring Security。

基于角色的控制,我们可以理解为Spring Security为我们维护了一张“白名单”,而登录就是去白名单里进行比对。

2.登录

2.1.默认用户

依赖:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

依赖引入后其实spring security就已经生效了,此时访问我们的接口会自动转跳spring security内置的登录页,验证通过后才会转跳到后端接口:

默认用户名:user

默认密码:会在日志中输出

2.2.自定义用户

spring security给我们提供了接口来配置security,其中就包括自定义用户和角色:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().
                //所有用户都可以访问/all
                        antMatchers("/all").permitAll().
                //admin可以访问/admin
                        antMatchers("/admin").hasRole("admin");
        //如果验证未通过,转跳spring security自带的登录页面进行登录
        //如果不配置此处的步骤,验证未通过则会直接返回403访问被拒绝。
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication()
                .withUser("user").password("123").roles("user")
                .and()
                .withUser("admin").password("456").roles("user", "admin");
    }
}

2.3.加密

完成上面的配置这时候我们再访问我们的接口,转跳登录页后输入我们定义的有对应访问用户名密码,比如admin 456,似乎就应该能正常访问到我们的接口了。但是实际上会报错:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

这是因为我们在定义用户的时候密码没有加密,自spring security 5.0开始就要求必须对密码进行加密,否则,在进行密码比较时,就会出现无法解析密码加密算法的异常。我们当前用的主流版本一定是5.0以上,所以加密是必须的。

spring security提供了一个PasswordEncoder接口给我们自定器,在加密器里可以通过方法重写来自定义加密过程。但是实际使用上不用费力去自己写一个加密算法,security给我们准备了多种加密器、多种加密方法,而且自己写的也肯定没有开源的稳定和好用。

spring security提供的加密器如下:

  • BCryptPasswordEncoder:这是最常用的加密算法之一,它使用哈希和随机盐来加密密码。

  • Pbkdf2PasswordEncoder:这也是一种密码加密算法,它使用基于密码的密钥导出函数(PBKDF2)来加密密码。

  • SCryptPasswordEncoder:这是一种基于内存的密码哈希算法,它使用大量的内存来防止散列碰撞攻击。

  • NoOpPasswordEncoder:这是一种不安全的加密算法,它仅仅是将明文密码作为加密后的密码。不建议在生产环境中使用。

我们就挑选BCryptPasswordEncoder来对密码进行加密,修改一下,在定义用户名密码的时候用BCryptPasswordEncoder来进行加密:

 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user").password("123").roles("user")
                .and()
                .withUser("admin").password("456").roles("user", "admin");
    }

加密之后我们再在登陆页面进行登录的时候就不能用明文的admin 456来进行登录了,需要将456使用同样的加密算法加密后的密文密码来登录。

2.4.绕过加密

在实际生产环境种使用密文密码当然是无可厚非的,但是在开发、测试阶段会很麻烦,有没有绕过的办法喃?有的,{noop}","no operation"的缩写,表示不执行任何操作。如果密码的前缀是"{noop}",则Spring Security会将其识别为明文密码,不进行加密,直接存储到数据库或内存中。可以将上面的代码改成:

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义两个用户user、admin,user对应user角色,admin对应admin角色
        auth.inMemoryAuthentication()
                .withUser("user").password("{noop}123").roles("user")
                .and()
                .withUser("admin").password("{noop}456").roles("user", "admin");
    }

2.5.怎么传递用户信息

由于我们的请求我们都是在登陆界面输入用户名密码,实际使用中不可能每次都这样去做,怎么把用户名、密码携带在请求里传给security喃?这却决于配置为哪种,支持两种方式:

  • 表单

  • 放在请求头中

表单:

后端配置:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                //指定用表单的方式登录
                .formLogin()
                //登录地址,不配置的话会有默认值,默认是login,可以用这个配置来设置新的请求页
                .loginPage("/login")
                //配置用户名的参数名,不配置的话会有默认值,默认是username
                .usernameParameter("username")
                //配置密码的参数名,不配置的话会有默认值,默认是password
                .passwordParameter("password")
                .permitAll()
    }

前端表单:

<form action="/login" method="post">
  <label for="username">Username:</label>
  <input type="text" id="username" name="username"><br><br>
  <label for="password">Password:</label>
  <input type="password" id="password" name="password"><br><br>
  <input type="submit" value="Submit">
</form>

当用户提交表单后,会向后端发送一个POST请求,请求的URL为/login,请求参数包含用户名和密码。

放在表头中:

在请求头中添加Authorization字段,将用户名和密码进行Base64编码后传递给后端进行验证。后端配置示例:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/", "/home").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
        .permitAll();
}

2.6.记住我

security支持“记住我”这个功能,是基于token来实现得,开启记住我后,登陆成功后会返给客户端一个cookie,用这个cookie来实现记住我的效果,配置示例如下:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().
                antMatchers("/all").permitAll().
                antMatchers("/admin").hasRole("admin").and()
                .formLogin().and()
                //开启记住我功能
                .rememberMe()
                //设置返回的cookie的键名
                .rememberMeParameter("remember-me")
                //设置过期时间
                .tokenValiditySeconds(7*24*60*60);
    }

3.登出

讲完登陆后再看登出就很好明白:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // 其他配置
        .logout()
            .logoutUrl("/logout") // 配置登出的URL
            .logoutSuccessUrl("/login") // 配置登出成功后的跳转URL
            .deleteCookies("remember-me") // 删除Cookie
            .permitAll(); // 允许所有用户访问登出URL
}

4.使用数据库

上文中我们都是用auth.inMemoryAuthentication()将定义的用户和角色信息放在内存中,在实际业务场景中有时候我们是需要将这些信息放在数据库中进行持久化的。spring security中有两种常用方式来将信息放在数据库中:

  • auth.jdbcAuthentication()

  • auth.userDetailsService()

4.1.jdbcAuthentication

使用这种方式时,严格规定了表结构,具体的建表语句在这里:

建出来后会有两个表:

这里要注意几点:

  • 权限前缀要加ROLE_,不然不会生效,enabled属性是用户是否生效1是0否

  • 密码一定要使用加密,不加密识别不了

接下来就是使用:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
            .usersByUsernameQuery("select username, password, enabled from users where username=?")
            .authoritiesByUsernameQuery("select username, authority from authorities where username=?")
            .passwordEncoder(new BCryptPasswordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
    }
}

4.2.userDetailsService

jdbcAuthentication这种方式中对表结构有严格要求,如果觉得太死,需要灵活一点,security还提供了userDetailsService这种方式灵活的来让我们灵活的使用。

我们可以自己实现UserDetailsService,重新loadUserByUsername方法,在方法中灵活的去将系统用户转为security的用户。

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从存储中获取系统用户
        SystemUser systemUser = userDao.getUser();
        //将系统用户的用户名密码来创建security的用户
        User.UserBuilder builder = User.builder();
        UserDetails user = builder.username(systemUser.getUserName()).password(systemUser.getPassword()).roles("admin").build();
        return user;
    }
}

5.自定义处理器

在Spring Security中,默认认证成功了就能访问到接口,但是如果我想认证成功后不直接访问接口而是做其他事情喃?spring security也提供了我们自定义操作完成后的处理过程的能力,我们可以使用自定义的处理器(Handler)来处理认证、授权、注销等操作。自定义处理器可以通过实现相应的接口或继承相应的类来完成。

以下是几个常用的自定义处理器的接口/类及其作用:

  1. AuthenticationSuccessHandler:用于认证成功后的处理,比如记录日志、跳转到指定页面等。

  2. AuthenticationFailureHandler:用于认证失败后的处理,比如记录日志、显示错误信息等。

  3. AccessDeniedHandler:用于处理访问被拒绝的情况,比如跳转到错误页面、记录日志等。

  4. LogoutSuccessHandler:用于注销成功后的处理,比如跳转到登录页面、删除Cookie等。

  5. InvalidSessionStrategy:用于处理无效的Session,比如跳转到登录页面、记录日志等。

在Spring Security中,我们可以通过配置来使用自定义的处理器。例如:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 处理认证成功的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/index");
    }
}
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 处理认证失败的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/login?error=true");
    }
}
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 处理访问拒绝的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/accessDenied");
    }
}
public class MyLogoutHandler implements LogoutHandler {

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 处理注销的逻辑,比如记录日志、清除缓存等
    }
}
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 处理注销成功的逻辑,比如记录日志、跳转页面等
        response.sendRedirect("/logoutSuccess");
    }
}

定义完后直接使用即可:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    //省略其他配置...
    
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    
    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    
    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    
    @Autowired
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;
    
    @Autowired
    private CustomInvalidSessionStrategy customInvalidSessionStrategy;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .successHandler(customAuthenticationSuccessHandler)
            .failureHandler(customAuthenticationFailureHandler)
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler)
            .and()
            .logout()
            .logoutSuccessHandler(customLogoutSuccessHandler)
            .and()
            .sessionManagement()
            .invalidSessionStrategy(customInvalidSessionStrategy);
    }
    
}

6.更多细粒度的控制

前文我们都是基于角色来进行访问控制的,稍加思考就会想到,光是靠角色来控制实际应用中可能不够,有时候还要基于HTTP METHOD、IP、权限等等,spring security当然给我们提供了这些更加细粒度的访问控制方法:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置安全规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        
            // 基于角色控制访问
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            
            // 基于权限控制访问
            .antMatchers("/article/**").hasAuthority("ARTICLE_VIEW")
            
            // 基于IP地址控制访问
            .antMatchers("/profile/**").hasIpAddress("192.168.1.0/24")
            
            // 基于方法表达式控制访问
            .antMatchers(HttpMethod.DELETE, "/article/**").access("hasRole('ADMIN') or hasAuthority('ARTICLE_DELETE')")
            .antMatchers(HttpMethod.PUT, "/article/**").access("hasRole('ADMIN') or hasAuthority('ARTICLE_EDIT')")
            
            // 其他请求需要进行身份认证
            .anyRequest().authenticated()
            
            .and()
            .formLogin()
            
            .and()
            .httpBasic();
    }

    // 配置用户信息
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        
            // 配置用户角色和权限
            .withUser("user").password("{noop}password").roles("USER").authorities("ARTICLE_VIEW")
            .and()
            .withUser("admin").password("{noop}password").roles("ADMIN").authorities("ARTICLE_VIEW", "ARTICLE_EDIT", "ARTICLE_DELETE");
    }
}

当然除了代码里可以配置,也提供了注解版本:

// 基于角色的访问控制注解
    @Secured("ROLE_ADMIN")
    public void adminOnlyMethod() {
        // 只有 ADMIN 角色的用户可以调用这个方法
    }
 
    // 基于角色的访问控制注解
    @RolesAllowed("ROLE_USER")
    public void userOnlyMethod() {
        // 只有 USER 角色的用户可以调用这个方法
    }
 
    // 基于方法调用之后的返回值进行访问控制
    @PostAuthorize("returnObject.owner == authentication.name")
    public Resource getResourceById(String id) {
        // 获取资源
        return resource;
    }
 
    // 在方法调用前进行集合过滤
    @PreFilter("filterObject.owner == authentication.name")
    public List<Resource> getResources(List<String> ids) {
        // 获取指定 ID 的资源列表
        return resources;
    }
 
    // 在方法调用后进行集合过滤
    @PostFilter("filterObject.owner == authentication.name")
    public List<Resource> getAllResources() {
        // 获取所有资源列表
        return resources;
    }
 
    // 基于SpEL表达式的访问控制注解,控制HTTP方法
    @PreAuthorize("hasRole('ADMIN') and #httpMethod == 'GET'")
    @GetMapping("/adminOnly")
    public ResponseEntity<String> adminOnlyEndpoint(HttpServletRequest request) {
        return ResponseEntity.ok("Only accessible to admins");
    }

7.原理简述

看上图,Spring Security的实现原理一目了然,其实它就是通过一个Servlet过滤器来挂载到应用程序的请求流程中。当一个请求到达应用程序时,它首先会被Spring Security的过滤器拦截,进行安全控制处理,然后将请求继续传递给应用程序的其他组件进行处理。

这里面的投票器不用深究,它的作用其实就是用于决定一个用户是否有访问特定资源的权限。投票器通过对安全上下文(SecurityContext)中的身份验证信息、访问控制列表(ACL)和其他因素进行评估,决定是否允许用户访问资源。

有关详解Spring Security的更多相关文章

  1. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  2. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  3. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  4. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  5. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  6. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  7. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  8. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

  9. ElasticSearch之 ik分词器详解 - 2

    IK分词器本文分为简介、安装、使用三个角度进行讲解。简介倒排索引众所周知,ES是一个及其强大的搜索引擎,那么它为什么搜索效率极高呢,当然和他的存储方式脱离不了关系,ES采取的是倒排索引,就是反向索引;常见索引结构几乎都是通过key找value,例如Map;倒排索引的优势就是有效利用Value,将多个含有相同Value的值存储至同一位置。分词器为了配合倒排索引,分词器也就诞生了,只有合理的利用Value,才会让倒排索引更加高效,如果一整个Value不进行任何操作直接进行存储,那么Value和key毫无区别。分词器Analyzer通常会对Value进行操作:一、字符过滤,过滤掉html标签;二、分

  10. Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解) - 2

    题外话:抑郁场,开局一小时只出A,死活想不来B,最后因为D题出锅ura才保住可怜的分。但咱本来就写不到DB-LongLegs(数论)本题题解法一学自同样抑郁的知乎作者幽血魅影的题解,有讲解原理。法二来着知乎巨佬cup-pyy(大佬说《不难发现》呜呜)题意三种操作:向上走mmm步向右走mmm步给自己一次走的步数加111,即使得m=m+1m=m+1m=m+1问从(0,0)(0,0)(0,0)走到(a,b)(a,b)(a,b)的最小操作次数,值得注意的是操作三不可逆。解析假设我们最终一步的大小增长到mmm,那么在这个过程中我能以[1,m][1,m][1,m](当步数增长到该数时)之间的任何数字向上或

随机推荐