当前位置:   article > 正文

芋道 Spring Security OAuth2 入门_spring-security-oauth2-autoconfigure

spring-security-oauth2-autoconfigure

芋道 Spring Security OAuth2 入门

 总阅读量:28123次

摘要: 原创出处 芋道 Spring Security OAuth2 入门 | 芋道源码 —— 纯源码解析博客 「芋道源码」欢迎转载,保留摘要,谢谢!



本文在提供完整代码示例,可见 GitHub - YunaiV/SpringBoot-Labs: 一个涵盖六个专栏:Spring Boot 2.X、Spring Cloud、Spring Cloud Alibaba、Dubbo、分布式消息队列、分布式事务的仓库。希望胖友小手一抖,右上角来个 Star,感恩 1024 的 lab-68-spring-security-oauth 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

《芋道 Spring Boot 安全框架 Spring Security 入门》文章中,艿艿分享了如何使用 Spring Security 实现认证与授权的功能,获得广大女粉丝的好评。

于是乎,艿艿准备再来分享一波 Spring Security OAuth 框架,看看在 Spring Security 如何实现 OAuth2.0 实现授权的功能。

旁白君:实际上艿艿很早写了一篇关于 Spring Security OAuth 的文章,考虑到版本太老,提供的示例又过于简单,所以本文也是该文章的升级版。

可能有胖友对 OAuth2.0 不是很了解,所以我们先来简单介绍下它。可能胖友看 OAuth2.0 的概念会有点懵逼,不要担心,后续看完艿艿提供的示例代码,会突然清晰的哈。

另外,阮一峰提供了几篇关于 OAuth2.0 非常不错的文章,推荐胖友去从瞅瞅。同时,本文也会直接引用它的内容,方便胖友统一理解。

1.1 OAuth2.0 是什么?

FROM 《维基百科 —— 开放授权》

OAuth(Open Authorization)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用

旁白君:很多团队,内部会采用 OAuth2.0 实现一个授权服务,避免每个上层应用或者服务重复开发。

OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。

每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

旁白君:如果胖友对接过微信网页授权功能,就会发现分成两种方式:静默授权、手动授权。前者只能获取到用户的 openid,而后者可以获取到用户的基本信息

OAuth2.0 是用于授权的行业标准协议。OAuth2.0 为简化客户端开发提供了特定的授权流,包括 Web 应用、桌面应用、移动端应用等。

旁白君:OAuth 1.0 协议体系本身存在一些问题,现已被各大开发平台逐渐废弃。

1.2 OAuth2.0 角色解释

在 OAuth2.0 中,有如下角色:

① Authorization Server:认证服务器,用于认证用户。如果客户端认证通过,则发放访问资源服务器的令牌

② Resource Server:资源服务器,拥有受保护资源。如果请求包含正确的访问令牌,则可以访问资源。

友情提示:提供管理后台、客户端 API 的服务,都可以认为是 Resource Server。

③ Client:客户端。它请求资源服务器时,会带上访问令牌,从而成功访问资源。

友情提示:Client 可以是浏览器、客户端,也可以是内部服务。

④ Resource Owner:资源拥有者。最终用户,他有访问资源的账号密码

友情提示:可以简单把 Resource Owner 理解成人,她在使用 Client 访问资源。

1.3 OAuth 2.0 运行流程

如下是 OAuth 2.0 的授权码模式的运行流程:

  • (A)用户打开客户端以后,客户端要求用户给予授权。
  • (B)用户同意给予客户端授权。
  • (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
  • (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
  • (E)客户端使用令牌,向资源服务器申请获取资源。
  • (F)资源服务器确认令牌无误,同意向客户端开放资源。

上述的六个步骤,B 是关键,即用户如何给客户端进行授权。有了授权之,客户端就可以获取令牌,进而凭令牌获取资源

友情提示:如果胖友有对接过三方开放平台,例如说微信、QQ、微博等三方登录,就会很容易理解这个步骤过程。

这个时候的资源,资源主要指的是三方开放平台的用户资料等等。

1.4 OAuth 2.0 授权模式

客户端必须得到用户的授权(Authorization Grant),才能获得访问令牌(Access Token)。

OAuth2.0 定义了四种授权方式:

  • 授权码模式(Authorization Code)
  • 密码模式(Resource Owner Password Credentials)
  • 简化模式(Implicit)
  • 客户端模式(Client Credentials)

其中,密码模式授权码模式比较常用。至于如何选择,艿艿这里先提前剧透下,后续慢慢细品。

FROM 《深度剖析 OAuth2 和微服务安全架构》

当然,对于黄框部分,对于笔者还是比较困惑的。笔者认为,第三方的单页应用 SPA ,也是适合采用 Authorization Code Grant 授权模式的。例如,《微信网页授权》 :

具体而言,网页授权流程分为四步:

  • 1、引导用户进入授权页面同意授权,获取 code
  • 2、通过 code 换取网页授权 access_token(与基础支持中的 access_toke n不同)
  • 3、如果需要,开发者可以刷新网页授权 access_token,避免过期
  • 4、通过网页授权 access_token 和 openid 获取用户基本信息(支持 UnionID 机制)

所以,艿艿猜测,之所以图中画的是 Implicit Grant 的原因是,受 Google 的 《OAuth 2.0 for Client-side Web Applications》 一文中,推荐使用了 Implicit Grant 。

当然,具体使用 Implicit Grant 还是 Authorization Code Grant 授权模式,没有定论。笔者,偏向于使用 Authorization Code Grant,对于第三方客户端的场景。

2. 密码模式

示例代码对应仓库:

本小节,我们来学习密码模式(Resource Owner Password Credentials Grant)

密码模式,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向授权服务器索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

旁白君:如果客户端和授权服务器都是自己公司的,显然符合。

  • (A)用户向客户端提供用户名和密码。
  • (B)客户端将用户名和密码发给授权服务器,向后者请求令牌
  • (C)授权服务器确认无误后,向客户端提供访问令牌。

下面,我们来新建两个项目,搭建一个密码模式的使用示例。如下图所示:

2.1 搭建授权服务器

创建 lab-68-demo02-authorization-server-with-resource-owner-password-credentials 项目,搭建授权服务器。

2.1.1 引入依赖

创建 pom.xml 文件,引入 Spring Security OAuth 依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>lab-68</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-68-demo02-authorization-server-with-resource-owner-password-credentials</artifactId>

    <properties>
        <!-- 依赖相关配置 -->
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <!-- 插件相关配置 -->
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 实现对 Spring MVC 的自动配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 实现对 Spring Security OAuth2 的自动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>

</project>

添加 spring-security-oauth2-autoconfigure 依赖,引入 Spring Security OAuth 并实现自动配置。同时,它也引入了 Spring Security 依赖。如下图所示:

2.1.2 SecurityConfig

创建 SecurityConfig 配置类,提供一个账号密码为「yunai/1024」的用户。代码如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.
                // 使用内存中的 InMemoryUserDetailsManager
                inMemoryAuthentication()
                // 不使用 PasswordEncoder 密码编码器
                .passwordEncoder(passwordEncoder())
                // 配置 yunai 用户
                .withUser("yunai").password("1024").roles("USER");
    }

}

我们通过 Spring Security 提供认证功能,所以这里需要配置一个用户。

友情提示:看不懂这个配置的胖友,后续可回《芋道 Spring Boot 安全框架 Spring Security 入门》重造下。

2.1.3 OAuth2AuthorizationServerConfig

创建 OAuth2AuthorizationServerConfig 配置类,进行授权服务器。代码如下:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 用户认证 Manager
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory() // <4.1>
                .withClient("clientapp").secret("112233") // <4.2> Client 账号、密码。
                .authorizedGrantTypes("password") // <4.2> 密码模式
                .scopes("read_userinfo", "read_contacts") // <4.2> 可授权的 Scope
//                .and().withClient() // <4.3> 可以继续配置新的 Client
                ;
    }

}

① 在类上添加 @EnableAuthorizationServer 注解,声明开启 OAuth 授权服务器的功能。

同时,继承 AuthorizationServerConfigurerAdapter 类,进行 OAuth 授权服务器的配置。

② #configure(AuthorizationServerEndpointsConfigurer endpoints) 方法,配置使用的 AuthenticationManager 实现用户认证的功能。其中,authenticationManager 是由「2.1.2 SecurityConfig」创建,Spring Security 的配置类。

③ #configure(AuthorizationServerSecurityConfigurer oauthServer) 方法,设置 /oauth/check_token 端点,通过认证后可访问。

友情提示:这里的认证,指的是使用 client-id + client-secret 进行的客户端认证,不要和用户认证混淆。

其中,/oauth/check_token 端点对应 CheckTokenEndpoint 类,用于校验访问令牌的有效性。

  • 在客户端访问资源服务器时,会在请求中带上访问令牌
  • 在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。

④ #configure(ClientDetailsServiceConfigurer clients) 方法,进行 Client 客户端的配置。

<4.1> 处,设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。

<4.2> 处,创建一个 Client 配置。如果要继续添加另外的 Client 配置,可以在 <4.3> 处使用 #and() 方法继续拼接。注意,这里的 .withClient("clientapp").secret("112233") 代码段,就是 client-id 和 client-secret

补充知识:可能会有胖友会问,为什么要创建 Client 的 client-id 和 client-secret 呢?

通过 client-id 编号和 client-secret,授权服务器可以知道调用的来源以及正确性。这样,即使“坏人”拿到 Access Token ,但是没有 client-id 编号和 client-secret,也不能和授权服务器发生有效的交互。

2.1.4 AuthorizationServerApplication

创建 AuthorizationServerApplication 类,授权服务器的启动类。代码如下:

@SpringBootApplication
public class AuthorizationServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthorizationServerApplication.class, args);
    }

}

2.1.5 简单测试

执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client

① POST 请求 http://localhost:8080/oauth/token 地址,使用密码模式进行授权。如下图所示:

请求说明:

  • 通过 Basic Auth 的方式,填写 client-id + client-secret 作为用户名与密码,实现 Client 客户端有效性的认证。
  • 请求参数 grant_type 为 "password",表示使用密码模式
  • 请求参数 username 和 password,表示用户的用户名与密码。

响应说明:

  • 响应字段 access_token 为访问令牌,后续客户端在访问资源服务器时,通过它作为身份的标识。
  • 响应字段 token_type 为令牌类型,一般是 bearer 或是 mac 类型。
  • 响应字段 expires_in 为访问令牌的过期时间,单位为秒。
  • 响应字段 scope 为权限范围

友情提示:/oauth/token 对应 TokenEndpoint 端点,提供 OAuth2.0 的四种授权模式。感兴趣的胖友,可以后续去撸撸。

② POST 请求 http://localhost:8080/oauth/check_token 地址,校验访问令牌的有效性。如下图所示:

请求和响应比较简单,胖友自己瞅瞅即可。

2.2 搭建资源服务器

创建 lab-68-demo02-resource-server 项目,搭建资源服务器。

2.2.1 引入依赖

创建 pom.xml 文件,引入 Spring Security OAuth 依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>lab-68</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-68-demo02-resource-server</artifactId>

    <properties>
        <!-- 依赖相关配置 -->
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <!-- 插件相关配置 -->
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 实现对 Spring MVC 的自动配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 实现对 Spring Security OAuth2 的自动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>

</project>

友情提示:和「2.1.1 引入依赖」小节,是一致的哈。

2.2.2 配置文件

创建 application.yml 配置文件,添加 Spring Security OAuth 相关配置。

server:
  port: 9090

security:
  oauth2:
    # OAuth2 Client 配置,对应 OAuth2ClientProperties 类
    client:
      client-id: clientapp
      client-secret: 112233
    # OAuth2 Resource 配置,对应 ResourceServerProperties 类
    resource:
      token-info-uri: http://127.0.0.1:8080/oauth/check_token # 获得 Token 信息的 URL
    # 访问令牌获取 URL,自定义的
    access-token-uri: http://127.0.0.1:8080/oauth/token

① security.oauth2.client 配置项,OAuth2 Client 配置,对应 OAuth2ClientProperties 类。在这个配置项中,我们添加了客户端的 client-id 和 client-secret

为什么要添加这个配置项呢?因为资源服务器会调用授权服务器的 /oauth/check_token 接口,而考虑到安全性,我们配置了该接口需要进过客户端认证

友情提示:这里艿艿偷懒了,其实单独给资源服务器配置一个 Client 的 client-id 和 client-secret。我们可以把资源服务器理解成授权服务器的一个特殊的客户端

② security.oauth2.resource 配置项,OAuth2 Resource 配置,对应 ResourceServerProperties 类。

这里,我们通过 token-info-uri 配置项,设置使用授权服务器的 /oauth/check_token 接口,校验访问令牌的有效性。

③ security.access-token-uri 配置项,是我们自定义的,设置授权服务器的 oauth/token 接口,获取访问令牌。因为稍后我们将在 LoginController 中,实现一个 /login 登录接口。

2.2.3 OAuth2ResourceServerConfig

创建 OAuth2ResourceServerConfig 类,进行资源服务器。代码如下:

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            // 设置 /login 无需权限访问
            .antMatchers("/login").permitAll()
            // 设置其它请求,需要认证后访问
            .anyRequest().authenticated()
            ;
    }

}

① 在类上添加 @EnableResourceServer 注解,声明开启 OAuth 资源服务器的功能。

同时,继承 ResourceServerConfigurerAdapter 类,进行 OAuth 资源服务器的配置。

② #configure(HttpSecurity http) 方法,设置 HTTP 权限。这里,我们设置 /login 接口无需权限访问,其它接口认证后可访问。

这样,客户端在访问资源服务器时,其请求中的访问令牌会被资源服务器调用授权服务器的 /oauth/check_token 接口,进行校验访问令牌的正确性。

2.2.4 ExampleController

创建 ExampleController 类,提供 /api/example/hello 接口,表示一个资源。代码如下:

@RestController
@RequestMapping("/api/example")
public class ExampleController {

    @RequestMapping("/hello")
    public String hello() {
        return "world";
    }

}

2.2.5 ResourceServerApplication

创建 ResourceServerApplication 类,资源服务器的启动类。代码如下:

@SpringBootApplication
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }

}

2.2.6 简单测试(第一弹)

执行 ResourceServerApplication 启动资源服务器。下面,我们来请求服务器的 <127.0.0.1:9090/api/example/hello> 接口,进行相应的测试。

① 首先,请求 <127.0.0.1:9090/api/example/hello> 接口,不带访问令牌,则请求会被拦截。如下图所示:

② 然后,请求 <127.0.0.1:9090/api/example/hello> 接口,带上错误的访问令牌,则请求会被拦截。如下图所示:

友情提示:访问令牌需要在请求头 "Authorization" 上设置,并且以 "Bearer " 开头。

③ 最后,请求 <127.0.0.1:9090/api/example/hello> 接口,带上正确的访问令牌,则请求会被通过。如下图所示:

2.2.7 LoginController

创建 LoginController 类,提供 /login 登录接口。代码如下:

@RestController
@RequestMapping("/")
public class LoginController {

    @Autowired
    private OAuth2ClientProperties oauth2ClientProperties;

    @Value("${security.oauth2.access-token-uri}")
    private String accessTokenUri;

    @PostMapping("/login")
    public OAuth2AccessToken login(@RequestParam("username") String username,
                                   @RequestParam("password") String password) {
        // <1> 创建 ResourceOwnerPasswordResourceDetails 对象
        ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
        resourceDetails.setAccessTokenUri(accessTokenUri);
        resourceDetails.setClientId(oauth2ClientProperties.getClientId());
        resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());
        resourceDetails.setUsername(username);
        resourceDetails.setPassword(password);
        // <2> 创建 OAuth2RestTemplate 对象
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails);
        restTemplate.setAccessTokenProvider(new ResourceOwnerPasswordAccessTokenProvider());
        // <3> 获取访问令牌
        return restTemplate.getAccessToken();
    }

}

在 /login 接口中,资源服务器扮演的是一个 OAuth 客户端的角色,调用授权服务器的 /oauth/token 接口,使用密码模式进行授权,获得访问令牌

① <1> 处,创建 ResourceOwnerPasswordResourceDetails 对象,填写密码模式授权需要的请求参数。

② <2> 处,创建 OAuth2RestTemplate 对象,它是 Spring Security OAuth 封装的工具类,用于请求授权服务器。

同时,将 ResourceOwnerPasswordAccessTokenProvider 设置到其中,表示使用密码模式授权。

友情提示:这一步非常重要,艿艿在这里卡了非常非常非常久,一度自闭要放弃。

③ <3> 处,调用 OAuth2RestTemplate 的 #getAccessToken() 方法,调用授权服务器的 /oauth/token 接口,进行密码模式的授权。

注意,OAuth2RestTemplate 是有状态的工具类,所以需要每次都重新创建。

2.2.8 简单测试(第二弹)

重新执行 ResourceServerApplication 启动资源服务器。下面,我们来进行 /login 接口的测试。

① 首先,请求 http://127.0.0.1:9090/login 接口,使用用户用户名密码进行登录,获得访问令牌。如下图所示:

响应结果和授权服务器的 /oauth/token 接口是一致的,因为就是调用它,嘿嘿~

② 然后,请求 <127.0.0.1:9090/api/example/hello> 接口,带刚刚的访问令牌,则请求会被通过。如下图所示:

3. 授权码模式

示例代码对应仓库:

本小节,我们来学习授权码模式(Authorization Code)

授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与授权务器进行互动。

旁白君:一般情况下,在有客户端的情况下,我们与第三方平台常常采用这种方式。

  • (A)用户访问客户端,后者将前者跳转到到授权服务器。
  • (B)用户选择是否给予客户端授权。
  • (C)假设用户给予授权,授权服务器将跳转到客户端事先指定的”重定向 URI”(Redirection URI),同时附上一个授权码
  • (D)客户端收到授权码,附上早先的”重定向 URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
  • (E)认证服务器核对了授权码重定向 URI,确认无误后,向客户端发送访问令牌

下面,我们来新建两个项目,搭建一个授权码模式的使用示例。如下图所示:

3.1 搭建授权服务器

复制出 lab-68-demo02-authorization-server-with-resource-owner-password-credentials 项目,修改搭建授权服务器。改动点如下图所示:

仅仅需要修改 OAuth2AuthorizationServerConfig 类,设置使用 "authorization_code" 授权码模式,并设置回调地址。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/83491
推荐阅读
相关标签