Spring Security in Action 第七章 配置授权:限制访问

news/2023/6/7 0:19:42

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总

文章目录

    • 7.1 基于权限和角色的访问限制
      • 7.1.1 根据用户权限限制api的访问
      • 7.1.2 根据用户角色限制所有api的访问权限
      • 7.1.3 限制对所有api的访问

本章包括

  • 定义权限和角色
  • 在controller上应用授权规则

几年前,我在美丽的喀尔巴阡山脉滑雪时,目睹了一个有趣的场景。有一群人在排队进入机舱,准备去滑雪场的顶端。一位知名的大网红在两名保镖的陪同下出现了。他自信满满地走上前去,期待着以他的名人身份跳过排队。走到队伍的最前面,他得到了一个惊喜。"请出示车票!"管理登机的人说:"你首先需要一张车票,其次,这次登机没有优先队,对不起,请站在后面排队.。"他指了指队伍的尽头。正如生活中的大多数情况,你是谁并不重要。对程序来说也正是如此。当试图访问一个特定的函数或数据时,你是谁并不重要!

到目前为止,我们只讨论了认证,正如你所了解的,这是应用程序识别资源调用者的过程。在前几章的例子中,我们并没有实现任何规则来决定是否批准一个请求。我们只关心系统是否认识这个用户。在大多数应用中,并不是一个用户可以访问系统中的每一个资源。在本章中,我们将讨论授权。授权是一个过程,在这个过程中,系统决定一个被识别的客户是否具有访问所请求资源的权限。

img
图7.1 授权是一个过程,在这个过程中,应用程序决定是否允许经过认证的实体访问一个资源。授权总是发生在认证之后。

在Spring Security中,一旦应用程序结束认证流程,它就会将请求委托给授权过滤器。该过滤器根据配置的授权规则允许或拒绝该请求(图7.2)。

image-20230119145419680

图7.2 当客户端发出请求时,认证过滤器对用户进行认证。认证成功后,认证过滤器将用户的详细信息存储在安全上下文中,并将请求转发给授权过滤器。授权过滤器决定该呼叫是否被允许。为了决定是否授权该请求,授权过滤器使用安全上下文中的细节。

7.1 基于权限和角色的访问限制

在这一节中,你将了解到授权和角色的概念,并用这些来保护你的应用程序的所有api。只有了解这些概念,然后才能将它们应用于现实世界的场景中,在这些场景中,不同的用户有不同的权限。根据用户拥有的权限,他们只能执行一个特定的动作。

在第三章中,我们实现了GrantedAuthority接口。当时我们没有使用GrantedAuthority,这个接口主要与授权过程有关。现在我们可以回到GrantedAuthority来研究它的目的。图7.3展示了UserDetails接口的约定和GrantedAuthority接口之间的关系。 一旦我们讨论完这个接口,我们将学会如何单独使用这些规则或为特定的请求使用。

image-20230119151303574

图7.3 一个用户有一个或多个权限(用户可以做的动作)。在认证过程中,UserDetailsService获得了关于用户的所有细节,包括权限。 应用程序在成功认证用户后,使用GrantedAuthority接口所代表的权限进行授权。

代码清单7.1显示了GrantedAuthority接口的定义。每个授权都表示用户可以对一系列程序资源操作的权限。每个权限都有对应的名字,对象的getAuthority()行为将其作为一个字符串返回。通常情况下,一个授权规则可以是这样的:"允许Jane删除产品记录,"或 “允许John读取文档记录”。在这些情况下,删除和读取是被授予的权限。应用程序允许用户Jane和John执行这些操作,这些操作的名称通常是读、写或删除。

代码清单 7.1 GrantedAuthority 接口 The GrantedAuthority contract

public interface GrantedAuthority extends Serializable {String getAuthority();
}

UserDetails是Spring Security中描述用户的接口,它有一个GrantedAuthority实例的集合,如图7.3所示。你可以允许一个用户拥有一个或多个权限。getAuthorities()方法返回GrantedAuthority实例的集合。在代码清单7.2中,我们可以查看UserDetails接口中的这个方法。我们可以实现这个方法,使其返回所有授予用户的权限。在认证结束后,这些授权是关于登录用户的细节的一部分,应用程序可以用它来授予权限。

代码清单7.2 来自UserDetails接口的getAuthorities(方法

public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();// 剩余代码省略
}

7.1.1 根据用户权限限制api的访问

在本节中,我们将讨论限制特定用户对api的访问。到目前为止,在我们的例子中,任何经过认证的用户都可以调用应用程序的任何api。从现在开始,你将学习如何定制这种访问。我们将写几个例子,让你了解用Spring Security应用这些限制的各种方法。

image-20230119152522042

图7.4 授权是用户在应用程序中可以执行的动作。基于这些操作,你可以实现授权规则。只有拥有特定权限的用户才能向一个端点提出特定的请求。例如,Jane只能读取和写入端点,而John可以读取、写入、删除和更新端点。

现在我们已经了解了UserDetails和GrantedAuthority接口以及它们之间的关系,现在是时候写一个应用授权规则的小程序了。通过这个例子,我们可以学到一些替代方案,根据用户的权限来配置对终端的访问。我们开始一个新项目,我把它命名为ch07-001-authorization。这里展示三种方法,你可以使用这些方法配置所提到的api:

  • hasAuthority() 只有拥有该权限的用户才能调用该api。
  • hasAnyAuthority() 可以接收一个以上的权限。用户必须至少拥有有一个指定的权限才能访问请求。

我推荐使用这个方法或hasAuthority()方法,因为它们很简单,这取决于你分配给用户的权限数量。这些都是简单的阅读配置,使我们的代码更容易理解。

  • access() 提供了配置访问权限的无限可能,因为应用程序是基于Spring表达式语言(SpEL)来构建授权规则的。然而,它使代码更难阅读和调试。由于这个原因,我推荐它作为较小的解决方案,而且只有在你不能应用hasAnyAuthority()或hasAuthority()方法的情况下使用。

pom.xml文件中唯一需要依赖的是spring-boot-starter-web和spring-boot-starter-security。这些依赖关系足以接近之前列举的所有三种解决方案。你可以在项目ch07-001-authorization中找到这个例子。

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

我们还在应用程序中添加一个api来测试我们的授权配置。

package com.hashnode.proj0001firstspringsecurity.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class HelloController {@GetMapping("/hello")public String hello(){return "Hello!";}
}

在一个配置类中,我们声明一个InMemoryUserDetailsManager作为我们的UserDetailsService,并添加两个用户,John和Jane,由这个实例来管理。每个用户都有不同的权限。你可以在下面的列表中看到如何做到这一点。

代码清单7.3 声明UserDetailsService并分配用户

package com.hashnode.proj0001firstspringsecurity.controller;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
public class ProjectConfig {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();UserDetails user1 = User.withUsername("john").password("12345").authorities("READ").build();UserDetails user2 = User.withUsername("jane").password("12345").authorities("WRITE").build();manager.createUser(user1);manager.createUser(user2);return manager;}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}
}

image-20230119154528588

接下来要做的是添加授权配置。在第2章中,我们配置了如何使所有的api对每个人都能访问。为了做到这一点,我们扩展了WebSecurityConfigurerAdapter类,并重载了configure()方法,如代码清单7.4所示:

代码清单7.4 使每个人都能访问所有的端点,而不需要认证

image-20230119155003820

authorizeRequests()方法可以让我们在api上指定授权规则。anyRequest()方法表示该规则适用于所有的请求,无论使用的是什么URL或HTTP方法。permitAll()方法允许访问所有请求,无论是否经过验证。

比方说,我们想确保只有拥有WRITE权限的用户才能访问所有的端点。对于我们的例子,这意味着只有Jane可以访问。这次我们可以实现我们的目标,根据用户的权限来限制访问。看看下面列表中的代码吧。

清单7.5 限制只有拥有WRITE权限的用户才能访问

package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;/*** @author Guowei Chi* @date 2023/1/19* @description:**/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {//省略部分代码@Overrideprotected void configure(HttpSecurity http) throws Exception {http.httpBasic();http.authorizeRequests().anyRequest().hasAuthority("WRITE");  //限制只有 WRITE 权限的用户可以访问}
}

可以看到,这里使用hasAuthority()方法替换了 permitAll()方法,这里给出了允许用户使用的权限名称作为hasAuthority()方法的参数。应用程序首先需要对请求进行认证,然后根据用户的权限,决定是否允许该调用。

接下来测试应用程序,分别使用不同用户调用api。当我们用用户Jane调用api时,HTTP响应状态是200 OK,我们看到的响应体是 "Hello!"当我们用用户John调用时,HTTP响应状态是403 Forbidden,我们得到一个空的响应体。

curl -u jane:12345 http://localhost:8080/hello
Hello!
curl -u john:12345 http://localhost:8080/hello
{
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}

以类似的方式,还可以使用hasAnyAuthority()方法,这个方法可以接收多个权限名称,表示只要用户有任意权限就可以访问某些api。

可以用hasAnyAuthority(“WRITE”)替换前面的hasAuthority(),在这种情况下,程序以同样的方式工作。然而,如果你将hasAuthority()替换为hasAnyAuthority(“WRITE”, “READ”),那么来自具有两种权限的用户的请求都会被接受。 在我们的例子中,应用程序允许来自John和Jane的请求。在下面的列表中,你可以看到如何应用hasAnyAuthority()方法。

代码清单 7.6 应用 hasAnyAuthority() 方法

package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {@Bean@Overridepublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();UserDetails user1 = User.withUsername("john").password("12345").authorities("READ").build();UserDetails user2 = User.withUsername("jane").password("12345").authorities("WRITE").build();manager.createUser(user1);manager.createUser(user2);return manager;}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.httpBasic();//允许具有WRITE或READ权限的用户访问http.authorizeRequests().anyRequest().hasAnyAuthority("WRITE","READ");}
}

为了指定基于用户权限的访问,第三种方式是access()方法。然而,access()方法更为通用。它接收一个指定授权条件的Spring表达式(SpEL)作为参数。这个方法很强大,而且它不仅仅指的是授权。然而,这个方法也使代码更难阅读和理解。出于这个原因,我推荐它作为最后的选择,而且只有在你不能应用本节前面介绍的hasAuthority()或hasAnyAuthority()方法之一的情况下。

为了使这个方法更容易理解,我首先把它作为用hasAuthority()和hasAnyAuthority()方法指定权限的替代方法。该方法必须提供一个Spring表达式作为方法的参数。然而,access()方法的优点是允许你通过你提供的表达式作为参数来定制规则。

注意 在大多数情况下,可以用hasAuthority()和hasAnyAuthority()方法实现所需的限制,推荐使用这些方法。只有在其他两个选项不合适,并且你想实现更多的通用授权规则时,才使用access()方法。

我们从一个简单的例子开始,以配合前面案例中的相同要求。 如果只需要测试用户是否有特定的权限,需要与access()方法一起使用的表达式可以是以下的一种。

  • hasAuthority(‘WRITE’)-提示用户需要WRITE授权来调用端点。
  • hasAnyAuthority(‘READ’, ‘WRITE’)-指定用户需要READ或WRITE权限中的一个。通过这个表达式,你可以列举出你想允许访问的所有权限。

请注意,这些表达式的名称与本节前面介绍的方法相同。下面的代码演示了如何使用access()方法。

代码清单7.7 使用access()方法来配置对api的访问

image-20230119162909952

从代码清单7.7中的例子可以看出,如果你将access()方法用于直接的要求,那么它是如何使语法复杂化的。在这种情况下,应该直接使用 hasAuthority() 或 hasAnyAuthority() 方法。但是access()方法并不全是不可取的,正如前面所说,它为你提供了灵活性。在现实世界的场景中,你可以用它来写更复杂的表达式,根据这些表达式,应用程序授予访问权。如果没有access()方法,你就无法实现这些场景。

在代码清单7.8中,我们发现如果access()方法不应用了表达式,就不容易写出来这样的权限控制。准确地说,代码清单7.8中的配置定义了两个用户,John和Jane,他们有不同的权限。用户John只有读取权限,而Jane有读取、写入和删除权限。api应该被那些有阅读权限的用户所访问,而不是那些有删除权限的用户。

代码清单7.8 用一个更复杂的表达式来应用access()方法

image-20230119163317445

当然,这只是一个假设的例子,但它足够简单,容易理解,也足够复杂,可以证明为什么access()方法更强大。

7.1.2 根据用户角色限制所有api的访问权限

在本节中,我们将讨论根据角色来限制对api的访问。角色是指用户可以做什么的另一种方式(图7.5)。你在现实世界的应用中也会发现这些,所以这就是为什么理解角色以及角色和权限之间的区别很重要。在本节中,我们将应用几个使用角色的例子,这样你就会知道应用程序使用角色的所有实际情况,以及如何为这些情况编写配置。

image-20230119164523120

图7.5 角色是粗粒度的。每个拥有特定角色的用户只能做该角色所授予的动作。在授权中应用这种理念时,根据用户在系统中的目的来允许请求。只有拥有特定角色的用户才能调用某个api。

Spring Security将权限理解为细粒度的特权,并对其施加限制。角色赋予用户一组行动的权限。例如在你的程序中,一个用户要么只拥有读取权限,要么拥有所有:读取、写入和删除权限。在这种情况下,如果认为那些只能阅读的用户拥有一个名为READER的角色,而其他用户拥有ADMIN的角色,拥有ADMIN角色意味着应用程序授予你读、写、更新和删除权限。程序中可能有更多的角色。例如,如果在某个时候,还需要一个只允许读和写的用户,你可以为你的应用程序创建第三个角色,名为MANAGER。

注意 当在应用程序中使用带有角色的方法时,我们将不必再定义权限。但是在应用程序中,需要定义一个角色来涵盖一个或多个用户被授权的行为。

用户可以自定义角色的名字,与授权相比,角色是粗粒度的,一个角色含有多个授权。 当定义一个角色时,它的名字应该以ROLLE_的前缀开始。在实现层面上,这个前缀指定了角色和权限之间的区别。在下一个代码清单中,看看我对前面的例子所做的修改。

清单7.9 为用户设置角色

package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;/*** @author Guowei Chi* @date 2023/1/19* @description:**/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {@Bean@Overridepublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();UserDetails user1 = User.withUsername("john").password("12345")//具有ROLE_前缀,表示角色.authorities("ROLE_ADMIN").build();UserDetails user2 = User.withUsername("jane").password("12345").authorities("ROLE_MANAGER").build();manager.createUser(user1);manager.createUser(user2);return manager;}//省略其他代码
}

要为用户角色设置约束,你可以使用以下方法之一。

  • hasRole()-接收一个参数,即应用程序授权请求的角色名称。
  • hasAnyRole()-接收作为参数的角色名称,应用程序为其批准请求。
  • access()-使用Spring表达式来指定应用程序授权请求的一个或多个角色。就角色而言,你可以使用hasRole()或hasAnyRole()作为SpEL表达式。

正如你所看到的,这些名称与第7.1.1节中介绍的方法类似。在下一个代码清单中,你可以看到configure()方法现在是什么样子。

package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {@Bean@Overridepublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();UserDetails user1 = User.withUsername("john").password("12345")//具有ROLE_前缀,表示角色.authorities("ROLE_ADMIN").build();UserDetails user2 = User.withUsername("jane").password("12345").authorities("ROLE_MANAGER").build();manager.createUser(user1);manager.createUser(user2);return manager;}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.httpBasic();//hasRole()方法现在指定了允许访问该api的角色。注意,ROLLE_前缀不会出现在这里。http.authorizeRequests().anyRequest().hasRole("ADMIN");}
}

注意 需要注意是,我们只使用ROLE_前缀来声明角色。但当我们使用角色时,我们只用它的名字来做。

测试结果如下:

curl -u john:12345 http://localhost:8080/hello
Hello!
curl -u jane:12345 http://localhost:8080/hello
{
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}

注意 确保你为role()方法提供的参数不能包括ROLLE_前缀。如果不小心在role()参数中包含了该前缀,该方法会抛出一个异常。简而言之,当使用authorities()方法时,要包括ROLLE_前缀。当使用role()方法时,不要包括ROLLE_前缀。

在下面的代码清单中,你可以看到当设计基于角色的访问时,使用role()方法而不是authorities()方法的正确使用方式。

代码清单7.11 用role()方法设置角色

image-20230119170707960

在第7.1.1节和第7.1.2节中,我们学习了如何使用access()方法来应用提及权限和角色的授权规则。一般来说,在一个应用程序中,授权限制是与权限和角色相关的。但重要的是要记住,access()方法是通用的。在我介绍的例子中,我主要是教你如何将它应用于权限和角色,但在实践中,它可以接受任何SpEL表达。它不需要与权限和角色相关联。一个直接的例子是将对端点的访问配置为只允许在晚上12:00以后。要解决这样的问题,你可以使用下面的SpEL表达式:

T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12, 0))

关于SpEL表达式的更多信息,请参见Spring文档:Core Technologies (spring.io)

通过access()方法,基本上可以实现任何种类的规则。这种可能性是无穷无尽的。只是别忘了,在应用程序中,我们总是努力使语法尽可能地简单。只有当你没有其他选择的时候,才会使你的配置复杂化。

7.1.3 限制对所有api的访问

在这一节中,我们将讨论限制对所有请求的访问。我们在第5章中了解到,通过使用permitAll()方法,可以允许对所有请求的访问。你还了解到,可以根据权限和角色来应用访问规则,不仅如此还可以拒绝所有请求。denyAll()方法与permitAll()方法正好相反。在接下来的代码清单中,你可以看到如何使用denyAll()方法。

代码清单7.12 使用denyAll(方法来限制对端点的访问

image-20230119171349472

那么,可以在什么地方使用这种限制呢?假设现在有一个api,以电子邮件地址作为入参,我们想要允许那些变量地址的值以.com结尾的请求,不希望应用程序接受任何其他格式的电子邮件地址。对于这个需求,我们可以使用一个正则表达式来分组符合规则的请求,然后使用denyAll()方法来使应用程序拒绝所有这些请求(图7.6)。

image-20230119171722316

图7.6 当用户调用端点并提供以.com结尾的参数值时,应用程序接受该请求。当用户调用端点并提供以.net结尾的电子邮件地址时,应用程序会拒绝该调用。为了实现这种行为,你可以对所有参数值不以.com结尾的端点使用denyAll()方法。

在微服务的场景,如图7.7所示,不同微服务有不同的功能,这些用例可以通过调用不同路径上的api进行访问。但是为了调用一个api,需要请求网关,在这个架构中,有两个网关服务。 在图7.7中,我把它们称为网关A和网关B。 客户端如果想访问/产品路径,就请求网关A。但对于/文章路径,客户必须请求网关B。每一个网关服务都被设计为拒绝所有对其他路径的请求,这些服务不为这些路径服务。这个简化的场景可以帮助你轻松理解denyAll()方法。在一个生产应用中,你可以在更复杂的架构中找到类似的情况。

image-20230119172207660

图7.7 通过网关A和网关B进行访问。每个网关只接收特定路径的请求,拒绝其他所有的请求。

总结

  • 授权是一个过程,在这个过程中,应用程序决定一个经过授权的请求是否被允许。授权总是发生在授权之后。
  • 在应用程序中,可以指定某些请求对未认证的用户是可以访问的。
  • 可以配置应用程序,通过使用denyAll()方法拒绝任何请求,或者使用permitAll()方法允许任何请求。

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

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

计算机word文本段落位置互换,word中调换位置 用word怎么使两个段落互换位置

1、首先在电脑上打开Word软件&#xff0c;然后在Word中打开需要处理的文档&#xff0c;以下面文档为演示。2、接下来在上方工具栏中选择“视图”-“大纲”。3、接下来在需要调整的段落前点击&#xff0c;将全段选中。4、然后选择下图红色箭头所指处的“上移”或“下移”&#x…

html复制粘贴的文字自动换行,如何解决Word中粘贴网页上的文字自动换行的现象...

在网页上复制了一段文字&#xff0c;粘贴到Word中的时候&#xff0c;却发现文字自动换行了&#xff0c;好好的一段话被回车在中间断的非常乱&#xff0c;这是什么原因呢&#xff0c;怎么取消Word粘贴时的自动换行呢?问题出现的原因&#xff1a;出现这种情况&#xff0c;大多不…

Word文档怎么删除html标签,word删除指定页面

如何指定删除word文档中某些页删除word文档中特定的一页中的方法&#xff1a; 一、选中特定页中的所有文本的方法&#xff1a; 方法1&#xff1a; 用鼠标从该页的首部拖动到尾部&#xff0c;即可选中该页中的所有文本。 方法2&#xff1a; 将光标定位在该页的首部&#xff0c;按…

python复制word段落,在Word文件中找到标题,然后使用python将整个段落复制到新的Word文件中...

I have the following situation:I have several hundred word files that contain company information. I would like to search these files for specific words to find specific paragraphs and copy just these paragraphs to new word files. Basically I just need to

word快速复制上一页_如何快速轻松地在Word中复制格式

word快速复制上一页Copying and pasting content in Word documents is a common task. However, you can also copy and paste formatting from one block of text (including images) to another. This can be handy if you want to apply the same formatting to multiple a…

C# 复制Word(复制全部内容、部分内容、页眉页脚)

本篇示例将介绍C# 复制Word文档的方法。根据不同的需要&#xff0c;我们将复制Word文档分三种情况来讲述&#xff0c;具体包括以下几点&#xff1a; 复制整个Word文档复制文档中的部分内容复制页眉或者页脚 工具使用Free Spire.Doc for .NET 6.3Visual StudioPS:在以下示例中需…

Python视频制作 MoviePy框架视频vfx视频效果示例

MoviePy 是一个用于视频编辑的 Python 模块,可用于视频和 GIF 的基本操作。将一系列图像组合成运动图片的视觉多媒体源。 使用视频 vfx 参数功能操作方法。 文章目录 素材移动效果视频或者图片移动时间相关变换函数指定时刻位置的帧定格屏幕范围内容固定循环视频素材淡出倍速…

Apollo星火计划学习笔记——Control 专项讲解(LQR)

文章目录1. 算法相关基础1.1 一阶倒立摆1.2 二自由度车辆横向跟踪偏差模型1.3 车辆横向跟踪偏差模型1.4 车辆横向跟踪偏差倒车模型1.5 轮胎侧偏角与侧偏刚度1.6 LQR 线性二次型问题:2. LQR代码解析2.1 WriteHeaders&#xff08;调试过程中的状态量&#xff09;2.2 LatControlle…