星空网 > 软件开发 > Java

Java 编程中的 OAuth 2.0 客户端,第 1 部分: 资源所有者密码凭据授权

原文出处: IBM - Varun Ojha

概述

OAuth 是一个开放的授权标准,允许客户端代表一个资源所有者获得访问受保护服务器资源的访问权。资源所有者可以是另一个客户端或最终用户。OAuth 还可以帮助最终用户将对其服务器资源的访问权限授权给第三方,而不必共享其凭据,比如用户名和密码。本系列文章遵循 RFC 6749 中所列出的 OAuth 2.0 授权框架。可以在 Internet Engineering Task Force 的网站上找到 RFC 6749 中列出的完整 OAuth 2.0 授权框架(请参阅 参考资料)。

授权批准

授权批准是一种凭据,可代表资源所有者用来访问受保护资源的权限。客户端使用此凭据获取访问令牌。访问令牌最终与请求一起发送,以便访问受保护资源。OAuth 2.0 定义了四种授权类型:

  1. 授权码
  2. 隐式
  3. 资源所有者密码凭据
  4. 客户端凭据

本文是由四部分组成的系列中的第 1 部分,将引导您使用上面列出的每种授权类型在 Java™ 编程中实现 OAuth 2.0 客户端。在第 1 部分中,我会告诉大家如何实现资源所有者密码凭据授权。本文详细介绍各种授权,并解释示例客户端代码,此代码可用于兼容 OAuth 2.0 的任何服务器接口,以支持此授权。在本文的最后,您应该对客户端实现有全面的了解,并准备好下载示例客户端代码,自己进行测试。

资源所有者密码凭据授权

当资源所有者对客户端有高度信任时,资源所有者密码凭据授权类型是可行的。此授权类型适合于能够获取资源所有者的用户名和密码的客户端。对于使用 HTTP 基础的现有企业客户端,或者想迁移到 OAuth 的摘要式身份验证,该授权最有用。然后,通过利用现有凭据来生成一个访问令牌,然后就可以实现迁移。

例如,Salesforce.com 添加了 OAuth 2.0 作为对其现有基础架构的一个授权机制。对于现有的客户端转变为这种授权方案,资源所有者密码凭据授权将是最方便的,因为他们只需使用现有的帐户详细信息(比如用户名和密码)来获取访问令牌。

图 1. 资源所有者密码凭据流

Java 编程中的 OAuth 2.0 客户端,第 1 部分: 资源所有者密码凭据授权

在 图 1 中所示的流程包括以下步骤:

  1. 资源所有者提供一个可信的 OAuth 2.0 客户端,并提供其用户名和密码。
  2. OAuth 2.0 客户端对授权服务器的令牌端点发出访问令牌请求,其中包括从资源所有者那里收到的凭据。在发出请求时,OAuth 2.0 客户端使用由授权服务器提供的凭据和授权服务器进行身份验证。
  3. 授权服务器对 OAuth 2.0 客户端进行身份验证,并验证资源所有者凭据,如果该凭据是有效的,那么授权服务器会颁发一个访问令牌。

访问令牌请求

对应于第二个步骤的访问令牌请求如 图 1 所示。

客户端对令牌端点(授权服务器)发出请求,采用 application/x-www-form-urlencoded 格式发送以下参数。

  • grant_type:必选项。必须将其值设置为 “password”
  • username:必选项。资源所有者的用户名。
  • password:必选项。资源所有者密码。
  • scope:可选项。访问请求的范围

如果客户端类型是机密的,或客户端获得了客户端凭据(或者被分配了其他身份验证要求),那么客户端必须向授权服务器进行身份验证。例如,客户端使用传输层安全性发出下列 HTTP 请求。

清单 1. 向授权服务器进行身份验证
1
2
3
4
5
POST /token HTTP/1.1
Host: server.example.com
Authorization:Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=varun&password=ab32vr



访问令牌响应

对应于上述步骤 C 的访问令牌响应如 图 1 所示。如果访问令牌请求是有效的,并且获得了授权,那么授权服务器将返回访问令牌和一个可选的刷新令牌。清单 2 显示了一个成功响应的示例。

清单 2. 成功的访问令牌响应
1
2
3
4
5
6
7
8
9
10
11
12
    HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
 
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}




如果请求无效,或者是未经授权的,那么授权服务器将会使用代码返回一个相应的错误消息。

设置

示例 OAuth 2.0 客户端被附加为可导入 Eclipse 环境中的 Java 项目。您需要将第三方依赖关系 JAR 文件下载到 Java 项目中的 lib 文件夹中。

依赖关系 JAR 文件

该项目使用以下 JAR 文件:

  • commons-codec-1.6.jar
  • commons-logging-1.1.1.jar
  • httpclient-4.2.5.jar
  • httpclient-cache-4.2.5.jar
  • httpcore-4.2.4.jar
  • httpmime-4.2.5.jar
  • json-simple-1.1.1.jar

在前六项中提到的 JAR 文件可以在 Http Components JAR 文件中找到。下载这些文件和 json-simple-1.1.1.jar 文件的链接,请参见 更多下载。确保已将下面这些可供下载的 JAR 文件复制到 Java 项目的 lib 文件夹。

先决条件

下载 Eclipse IDE for Java EE developers,以便设置开发环境,并导入附加项目。相关的链接请参见 更多下载。

OAuth 2.0 客户端

此处讨论的 OAuth 2.0 客户端实现了资源所有者密码凭据授权。本系列文章的后续部分将描述其余授权类型,并继续更新客户端代码。

输入参数

使用在示例客户端代码下载(请参阅 Download)中提供的 Oauth2Client.config 属性文件向客户端提供所需的输入参数。

  • scope:这是一个可选参数。它代表访问请求的范围。由服务器返回的访问令牌只可以访问 scope 中提到的服务。
  • grant_type:需要将这个参数设置为 "password",表示资源所有者密码凭据授权。
  • username:用于登录到资源服务器的用户名。
  • password:用于登录到资源服务器的密码。
  • client_id:注册应用程序时由资源服务器提供的客户端或使用者 ID。
  • client_secret:注册应用程序时由资源服务器提供的客户端或使用者的密码。
  • access_token:授权服务器响应有效的和经过授权的访问令牌请求时返回的访问令牌。作为该请求的一部分,您的用户名和密码将用于交换访问令牌。
  • refresh_token:这是一个可选参数,由授权服务器在响应访问令牌请求时返回。然而,大多数端点(比如 Salesforce、IBMWebSphere® Application Server 和 IBM DataPower)对资源所有者密码凭据授权不返回刷新令牌。因此,我的客户端实现不打算考虑刷新令牌。
  • authenticatation_server_url:这表示令牌端点。批准和重新生成访问令牌的所有请求都必须发送到这个 URL
  • resource_server_url:这表示需要联系的资源服务器的 URL,通过将授权标头中的访问令牌传递给它来访问受保护的资源。
清单 3. Oauth2Client 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Properties config = OAuthUtils.getClientConfigProps    (OAuthConstants.CONFIG_FILE_PATH);
String resourceServerUrl = config.getProperty(OAuthConstants.RESOURCE_SERVER_URL);     
String username = config.getProperty(OAuthConstants.USERNAME);
String password = config.getProperty(OAuthConstants.PASSWORD);
String grantType = config.getProperty(OAuthConstants.GRANT_TYPE);
String authenticationServerUrl = config
        .getProperty(OAuthConstants.AUTHENTICATION_SERVER_URL);
 
 if (!OAuthUtils.isValid(username)
   || !OAuthUtils.isValid(password)
   || !OAuthUtils.isValid(authenticationServerUrl)
   || !OAuthUtils.isValid(grantType)) {
 System.out
       .println("Please provide valid values for username, password,
                       authentication server url and grant type");
 System.exit(0);
 
  }
 
if (!OAuthUtils.isValid(resourceServerUrl)) {
// Resource server url is not valid.
//Only retrieve the access token
System.out.println("Retrieving Access Token");
OAuth2Details oauthDetails = OAuthUtils.createOAuthDetails(config);
String accessToken = OAuthUtils.getAccessToken(oauthDetails);
System.out
.println("Successfully retrieved Access token
 for Password Grant:" + accessToken);
}
else {
// Response from the resource server must be in Json or
//Urlencoded or
System.out.println("Resource endpoint url:" + resourceServerUrl);
System.out.println("Attempting to retrieve protected resource");
OAuthUtils.getProtectedResource(config);
     }




在 清单 3 中的客户端代码读取 Oauth2Client.config 文件中所提供的输入参数。usernamepassword、 authentication server url 和 grant type 的有效值是强制性的。如果配置文件中所提供的资源服务器 URL 是有效的,那么客户端会尝试检索该 URL 中提供的受保护资源。否则,客户端只对授权服务器发出访问令牌请求,并取回访问令牌。以下部分说明了负责检索受保护资源和访问令牌的代码。

访问受保护资源

清单 4 中的代码演示了如何使用访问令牌来访问受保护的资源。

清单 4. 访问受保护资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
String resourceURL =
config.getProperty(OAuthConstants.RESOURCE_SERVER_URL);
OAuth2Details oauthDetails = createOAuthDetails(config);
HttpGet get = new HttpGet(resourceURL);
get.addHeader(OAuthConstants.AUTHORIZATION,
getAuthorizationHeaderForAccessToken(oauthDetails
.getAccessToken()));
DefaultHttpClient client = new DefaultHttpClient();
HttpResponse response = null;
int code = -1;
  try {
    response = client.execute(get);
    code = response.getStatusLine().getStatusCode();
    if (code >= 400) {
      // Access token is invalid or expired.
      // Regenerate the access token
      System.out.println("Access token is invalid
      or expired.Regenerating access token....");
      String accessToken = getAccessToken(oauthDetails);
      if (isValid(accessToken)) {
    // update the access token
      // System.out.println("New access token:" + accessToken);
      oauthDetails.setAccessToken(accessToken);
      get.removeHeaders(OAuthConstants.AUTHORIZATION);
      get.addHeader(OAuthConstants.AUTHORIZATION,
      getAuthorizationHeaderForAccessToken(oauthDetails
    .getAccessToken()));
      get.releaseConnection();
      response = client.execute(get);
      code = response.getStatusLine().getStatusCode();
    if (code >= 400) {
    throw new RuntimeException("Could not
  access protected resource.
  Server returned http code:"+ code);
 
     }
 
   } else {
    throw new RuntimeException("Could not
       regenerate access token");
    }
 
 }
 
   handleResponse(response);




注意

  • 此方法使用从配置文件检索到的值来填充 OauthDetails bean。
  • 顾名思义,这种方法将尝试从资源服务器检索受保护的资源,因此,您需要创建一个简单的 HttpGet 方法。
  • 为了向资源服务器进行身份验证,需要将访问令牌作为 Authorization 标头的一部分发送。示例:Authorization:Bearer accessTokenValue
  • 创建一个 DefaultHttpClient 对资源服务器发出一个 get 请求。
  • 如果从资源服务器收到的响应代码是 403 或 401,则用于身份验证的访问令牌可能已过期或无效。
  • 下一步是重新创建访问令牌(清单 5)。
  • 成功地重新生成访问令牌之后,更新 OauthDetails bean 中的访问令牌值。用新的访问令牌值替换 get 方法中现有的 Authorization 标头。
  • 现在发出对受保护资源的另一个访问请求。
  • 如果访问令牌有效,而且资源服务器的 URL 也是正确的,那么您应该可以在控制台中看到响应内容。

重新生成过期的访问令牌

清单 5 中的代码将会处理已过期访问令牌的重新生成。

清单 5. 重新生成过期的访问令牌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
HttpPost post = new HttpPost(
oauthDetails.getAuthenticationServerUrl());
String clientId = oauthDetails.getClientId();
String clientSecret = oauthDetails.getClientSecret();
String scope = oauthDetails.getScope();
 
List<BasicNameValuePair> parametersBody =
new ArrayList<BasicNameValuePair>();
parametersBody.add(new BasicNameValuePair(OAuthConstants.GRANT_TYPE,
oauthDetails.getGrantType()));
parametersBody.add(new BasicNameValuePair(OAuthConstants.USERNAME,
oauthDetails.getUsername()));
parametersBody.add(new BasicNameValuePair(OAuthConstants.PASSWORD,
oauthDetails.getPassword()));
 
if (isValid(clientId)) {
   parametersBody.add(new BasicNameValuePair
     (OAuthConstants.CLIENT_ID,clientId));
 }
if (isValid(clientSecret)) {
   parametersBody.add(new BasicNameValuePair(
   OAuthConstants.CLIENT_SECRET, clientSecret));
 }
if (isValid(scope)) {
   parametersBody.add(new BasicNameValuePair
     (OAuthConstants.SCOPE,scope));
 }
 
DefaultHttpClient client = new DefaultHttpClient();
HttpResponse response = null;
String accessToken = null;
  try {
   post.setEntity(new UrlEncodedFormEntity(parametersBody,
      HTTP.UTF_8));
 
   response = client.execute(post);
   int code = response.getStatusLine().getStatusCode();
   if (code >= 400) {
   System.out.println("Authorization
      server expects Basic authentication");
   // Add Basic Authorization header
   post.addHeader(
   OAuthConstants.AUTHORIZATION,
   getBasicAuthorizationHeader(oauthDetails.getUsername(),
   oauthDetails.getPassword()));
   System.out.println("Retry with login credentials");
   post.releaseConnection();
   response = client.execute(post);
   code = response.getStatusLine().getStatusCode();
   if (code >= 400) {
   System.out.println("Retry with client credentials");
   post.removeHeaders(OAuthConstants.AUTHORIZATION);
   post.addHeader(
   OAuthConstants.AUTHORIZATION,
      getBasicAuthorizationHeader(
    oauthDetails.getClientId(),
   oauthDetails.getClientSecret()));
   post.releaseConnection();
   response = client.execute(post);
   code = response.getStatusLine().getStatusCode();
   if (code >= 400) {
   throw new RuntimeException(
   "Could not retrieve access token for user:"
   oauthDetails.getUsername());
      }
       }
     }
   Map<String, String> map = handleResponse(response);
   accessToken = map.get(OAuthConstants.ACCESS_TOKEN);
   } catch (ClientProtocolException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
   } catch (IOException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
   }
 
   return accessToken;




注意

  • 这种方法生成一个 HttpPost 请求,并获得身份验证服务器的 URL。
  • Post 请求以 URL 编码参数的形式发送 usernamepassword,以及可选的 scope,将它们作为有效载荷的一部分。
  • 有些授权服务器还会要求您发送 client_id 和 client_secret 作为此请求有效载荷的一部分。
  • 如果 client_idclient_secret 和 scope 的值不为空,那么它们也将作为有效载荷的一部分被发送。
  • 按照 OAuth 2.0 授权框架,客户端应该使用客户端凭据,或使用在发出访问令牌请求时由服务器所提供的其他任何凭据来设置 Authorization 标头。但是,这受制于授权服务器实现。客户端代码发出初始请求时无需添加基本身份验证标头。如果服务器返回一个未经授权的响应,客户端随后试图通过登录和客户端凭据进行身份验证。
  • OAuth 2.0 规定,需要采用 JSON 格式来发送访问令牌响应。但为了灵活性,我还添加了实用程序方法来处理来自服务器的

测试客户端

在本节中,我将讨论如何建立一个 OAuth 2.0 兼容的端点并用它来测试客户端。

在 Salesforce.com 注册

Salesforce.com 对于资源所有者密码凭据授权是一个很好的用例。如果用户有登录 Salesforce.com 的凭据,并希望自己的客户端转变为 OAuth 2.0 身份验证,那么他需要做的就是在 Salesforce 注册自己的应用程序,以获得客户端凭据。现在可以用这些客户端凭据以及他现有的登录凭据从授权服务器中获得访问令牌。

  1. 如果您以前没有在 Salesforce.com 注册过,请立即注册。请参阅 参考资料 。
  2. 单击 Login,然后单击 Sign up for free
  3. 完成注册,获得您的凭据。
  4. 除了用户名和密码之外,您还将获得一个安全令牌。您提供的用于发出访问令牌请求的密码必须是您的密码和安全令牌的串联。(示例:password12312123)。
  5. 有关如何在 salesforce.com 中创建应用程序,请参阅 参考资料 中有用的文章链接。

运行客户端

现在,您已经完成了 Salesforce.com 中的注册,您可以测试客户端并从服务器检索受保护的信息。

  • 将本文中讨论的 Java 项目导入到 Eclipse 工作区(请参阅 参考资料)。
  • 下载依赖关系 JAR 文件,并将其复制到项目的 lib 文件夹中(请参阅 更多下载 )。
  • 导航到 resources/com/ibm/oauth/Oauth2Client.config 文件,并填写 usernamepassword(追加安全令牌)、client_idclient_secret 和 authorization server URL 的值。
  • 打开 Oauth2Client.java 并运行它。

访问令牌输出

您应该在控制台窗口看到下面的输出。

1
2
3
4
5
6
7
8
9
10
Retrieving Access Token
encodedBytes dmVybi5vamhhQGdtYWlsL.......
 
********** Response Received **********
  instance_url = https://ap1.salesforce.com
  issued_at = 1380106995639
  signature = LtMjTrmoBbvVfZ6+qT5Un1UioHaV9KIOK7ayQTmJzCg=
  id = https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO
  access_token = 00D90000000mQaY!AQ8AQEn0rLDMvxrP9WgY3Blc.......
Successfully retrieved Access token for Password Grant:00D90000000mQaY!AQ8AQEn0rLDMvxrP9WgY3Bl......




从 Salesforce.com 检索用户信息

现在,您已经有了访问令牌和 ID,可以向 Salesforce.com 发出请求,通过使用 OAuth 2.0 进行身份验证来访问您的帐户信息。

  • 用访问令牌更新 Oauth2Client.confg 文件,并使用作为响应的一部分返回的 id 值来填充资源服务器 URL 属性。
  • 再次运行 Oauth2Client.java。

输出

您应该在控制台窗口看到类似于下面的输出。

清单 6. 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Resource endpoint URL: https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO
Attempting to retrieve protected resource
********** Response Received **********
photos = {"thumbnail":"https:\/\/c.ap1.content.force.com\/profilephoto\/005\/T","picture":"https:\/\
/c.ap1.content.force.com\/profilephoto\/005\/F"}
urls =
{"enterprise":"https:\/\/ap1.salesforce.com\/services\/Soap\/c\/{version}\/00D90000000mQaY","sobjects":
"https:\/\/ap1.salesforce.com\/services\/data\/v{version}\/sobjects\/","partner":"https:\/\
/ap1.salesforce.com\/services\/Soap\/u\/{version}\/00D90000000mQaY","search":"https:\/\
/ap1.salesforce.com\/services\/data\/v{version}\/search\/","query":"https:\/\/ap1.salesforce.com\
/services\/data\/v{version}\/query\/","users":"https:\/\/ap1.salesforce.com\/services\/data\/v{version}\
/chatter\/users","profile":"https:\/\/ap1.salesforce.com\/00590000001HCB7AAO","metadata":"https:\/\
/ap1.salesforce.com\/services\/Soap\/m\/{version}\/00D90000000mQaY","rest":"https:\/\/ap1.salesforce.com\
/services\/data\/v{version}\/","groups":"https:\/\/ap1.salesforce.com\/services\/data\/v{version}\
/chatter\/groups","feeds":"https:\/\/ap1.salesforce.com\/services\/data\/v{version}\/chatter\/feeds",
"recent":"https:\/\/ap1.salesforce.com\/services\/data\/v{version}\/recent\/","feed_items":"https:\/
\/ap1.salesforce.com\/services\/data\/v{version}\/chatter\/feed-items"}
  asserted_user = true
  active = true
  organization_id = 00D90000000mQaYEAU
  nick_name = vern.ojha1....
  display_name = varun ojha
  user_type = STANDARD
  user_id = ***********
  status = {"body":null,"created_date":null}
  last_name = ojha
  username = vern.ojha.....
  utcOffset = -28800000
  language = en_US
  locale = en_US
  first_name = varun
  last_modified_date = 2013-06-04T07:43:42.000+0000
  id = https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO
  email = vern.ojha@gmail.com




如您所见,您可以通过使用 OAuth 2.0 进行身份验证,成功获取用户信息。在配置文件中提供的访问令牌过期后,客户端将会自动重新生成访问令牌,并使用它来检索在资源服务器 URL 中提供的受保护资源。

用 IBM 端点测试客户端

客户端也已经成功通过 OAuth 2.0 兼容的 IBM 端点的测试,即 IBM WebSphere Application Server 和 IBM DataPower。请参阅 参考资料 的链接,“使用 OAuth:在 WebSphere Application Server 中启用 OAuth 服务提供程序”,这是一个非常好的资源,介绍了如何在 WebSphere Application Server 上设置 OAuth 2.0。

在您的应用服务器上设置了 OAuth 2.0 后,客户端所需的输入与 Salesforce.com 演示中的相同。

结束语

本文是该系列文章的第一部分,阐述了资源所有者密码凭据授权的基础知识。本文演示了如何在 Java 编程中编写一个通用的 OAuth 2.0 客户端,以连接到 OAuth 2.0 兼容的多个端点,并从中获取受保护的资源。示例客户端被附加为一个 Java 项目,以使您能够迅速导入项目到 Eclipse 工作区,并开始测试。在本系列文章的后续部分中,我将介绍在 OAuth 2.0 授权框架中列出的其余三种授权类型。在后续文章中,客户端代码将会获得更新,以反映这些授权类型及特性,比如发布到某个资源服务器和处理 SSL。

下载

描述名字大小
示例客户端代码OAuth20.zip19KB

  都说程序员的工资高,却很少了解他们加班的痛苦,你是不是每次也在心里想,按时间折算下来这个工资都给少了,于是会想在心里呐喊,要么涨工资,要么涨工资,要么涨工资,为什么??因为不让我们加班,这是不可能的!!!

  想要颠覆自己的工作模式吗?想要减少自己的加班时间吗?加入我们,和我们一起探寻属于我们程序员的自由模式吧!

  一款针对程序员的原生APP,以共享知识技能为目的在线互动交互平台。

  我们拥有高达近20人顶尖的技术团队,以及优秀的产品及运营团队。团队领军人物均在行业内有10年以上的丰富经验。

  现在我们正在招募原始的参与英雄,您将同我们一起改变程序员的工作方式,改变程序员的世界!同时也会有丰厚的报酬。作为我们的原始的参与者,您将同我们一起体验这款程序员神器,您可以提出专业的建议,我们会虚心采纳。每一个人都会是英雄,而您就会是我们需要的英雄!同时您也可以邀请您的朋友一起参与这场英雄的招募互动。

  我们不会耽误你太多时间,我们只需要您的专业看法,只要您从一个月内抽出1个小时,以后您每天都可以节省两个小时,一切都是为了我们自己!

  来?还是不来?

  接头人暗号:1955246408 (QQ)




原标题:Java 编程中的 OAuth 2.0 客户端,第 1 部分: 资源所有者密码凭据授权

关键词:JAVA

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

哪里是最佳的商标注册地点?:https://www.kjdsnews.com/a/1388988.html
哪些网站可以帮助你快速核名注册商标:https://www.kjdsnews.com/a/1388989.html
哪些平台最适合购买商标?:https://www.kjdsnews.com/a/1388990.html
商标与商标标志的区别及申请要点:https://www.kjdsnews.com/a/1388991.html
商标与R标的区别有哪些?:https://www.kjdsnews.com/a/1388992.html
商标与R标的区别及其重要性:https://www.kjdsnews.com/a/1388993.html
月活用户超20亿!万亿市值巨头对中国商家进一步开闸放流 :https://www.kjdsnews.com/a/1836412.html
九寨沟周围必去的景点推荐:https://www.vstour.cn/a/363190.html
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流