手把手教大家在 gRPC 中使用 JWT 完成身份校驗(yàn)

@[toc]
上篇文章松哥和小伙伴們聊了在 gRPC 中如何使用攔截器,這些攔截器有服務(wù)端攔截器也有客戶端攔截器,這些攔截器的一個(gè)重要使用場(chǎng)景,就是可以進(jìn)行身份的校驗(yàn)。當(dāng)客戶端發(fā)起請(qǐng)求的時(shí)候,服務(wù)端通過攔截器進(jìn)行身份校驗(yàn),就知道這個(gè)請(qǐng)求是誰(shuí)發(fā)起的了。今天松哥就來(lái)通過一個(gè)具體的案例,來(lái)和小伙伴們演示一下 gRPC 如何結(jié)合 JWT 進(jìn)行身份校驗(yàn)。

1. JWT 介紹

1.1 無(wú)狀態(tài)登錄

1.1.1 什么是有狀態(tài)

有狀態(tài)服務(wù),即服務(wù)端需要記錄每次會(huì)話的客戶端信息,從而識(shí)別客戶端身份,根據(jù)用戶身份進(jìn)行請(qǐng)求的處理,典型的設(shè)計(jì)如 Tomcat 中的 Session。例如登錄:用戶登錄后,我們把用戶的信息保存在服務(wù)端 session 中,并且給用戶一個(gè) cookie 值,記錄對(duì)應(yīng)的 session,然后下次請(qǐng)求,用戶攜帶 cookie 值來(lái)(這一步有瀏覽器自動(dòng)完成),我們就能識(shí)別到對(duì)應(yīng) session,從而找到用戶的信息。這種方式目前來(lái)看最方便,但是也有一些缺陷,如下:

  • 服務(wù)端保存大量數(shù)據(jù),增加服務(wù)端壓力
  • 服務(wù)端保存用戶狀態(tài),不支持集群化部署

1.1.2 什么是無(wú)狀態(tài)

微服務(wù)集群中的每個(gè)服務(wù),對(duì)外提供的都使用 RESTful 風(fēng)格的接口。而 RESTful 風(fēng)格的一個(gè)最重要的規(guī)范就是:服務(wù)的無(wú)狀態(tài)性,即:

  • 服務(wù)端不保存任何客戶端請(qǐng)求者信息
  • 客戶端的每次請(qǐng)求必須具備自描述信息,通過這些信息識(shí)別客戶端身份

那么這種無(wú)狀態(tài)性有哪些好處呢?

  • 客戶端請(qǐng)求不依賴服務(wù)端的信息,多次請(qǐng)求不需要必須訪問到同一臺(tái)服務(wù)器
  • 服務(wù)端的集群和狀態(tài)對(duì)客戶端透明
  • 服務(wù)端可以任意的遷移和伸縮(可以方便的進(jìn)行集群化部署)
  • 減小服務(wù)端存儲(chǔ)壓力

1.2 如何實(shí)現(xiàn)無(wú)狀態(tài)

無(wú)狀態(tài)登錄的流程:

  • 首先客戶端發(fā)送賬戶名/密碼到服務(wù)端進(jìn)行認(rèn)證
  • 認(rèn)證通過后,服務(wù)端將用戶信息加密并且編碼成一個(gè) token,返回給客戶端
  • 以后客戶端每次發(fā)送請(qǐng)求,都需要攜帶認(rèn)證的 token
  • 服務(wù)端對(duì)客戶端發(fā)送來(lái)的 token 進(jìn)行解密,判斷是否有效,并且獲取用戶登錄信息

1.3 JWT

1.3.1 簡(jiǎn)介

JWT,全稱是 Json Web Token, 是一種 JSON 風(fēng)格的輕量級(jí)的授權(quán)和身份認(rèn)證規(guī)范,可實(shí)現(xiàn)無(wú)狀態(tài)、分布式的 Web 應(yīng)用授權(quán):

image

JWT 作為一種規(guī)范,并沒有和某一種語(yǔ)言綁定在一起,常用的 Java 實(shí)現(xiàn)是 GitHub 上的開源項(xiàng)目 jjwt,地址如下:https://github.com/jwtk/jjwt

1.3.2 JWT數(shù)據(jù)格式

JWT 包含三部分?jǐn)?shù)據(jù):

  • Header:頭部,通常頭部有兩部分信息:

    • 聲明類型,這里是JWT
    • 加密算法,自定義

我們會(huì)對(duì)頭部進(jìn)行 Base64Url 編碼(可解碼),得到第一部分?jǐn)?shù)據(jù)。

  • Payload:載荷,就是有效數(shù)據(jù),在官方文檔中(RFC7519),這里給了7個(gè)示例信息:

    • iss (issuer):表示簽發(fā)人
    • exp (expiration time):表示token過期時(shí)間
    • sub (subject):主題
    • aud (audience):受眾
    • nbf (Not Before):生效時(shí)間
    • iat (Issued At):簽發(fā)時(shí)間
    • jti (JWT ID):編號(hào)

這部分也會(huì)采用 Base64Url 編碼,得到第二部分?jǐn)?shù)據(jù)。

  • Signature:簽名,是整個(gè)數(shù)據(jù)的認(rèn)證信息。一般根據(jù)前兩步的數(shù)據(jù),再加上服務(wù)的的密鑰secret(密鑰保存在服務(wù)端,不能泄露給客戶端),通過 Header 中配置的加密算法生成。用于驗(yàn)證整個(gè)數(shù)據(jù)完整和可靠性。

生成的數(shù)據(jù)格式如下圖:

image

注意,這里的數(shù)據(jù)通過 . 隔開成了三部分,分別對(duì)應(yīng)前面提到的三部分,另外,這里數(shù)據(jù)是不換行的,圖片換行只是為了展示方便而已。

1.3.3 JWT 交互流程

流程圖:

image

步驟翻譯:

  1. 應(yīng)用程序或客戶端向授權(quán)服務(wù)器請(qǐng)求授權(quán)
  2. 獲取到授權(quán)后,授權(quán)服務(wù)器會(huì)向應(yīng)用程序返回訪問令牌
  3. 應(yīng)用程序使用訪問令牌來(lái)訪問受保護(hù)資源(如 API)

因?yàn)?JWT 簽發(fā)的 token 中已經(jīng)包含了用戶的身份信息,并且每次請(qǐng)求都會(huì)攜帶,這樣服務(wù)的就無(wú)需保存用戶信息,甚至無(wú)需去數(shù)據(jù)庫(kù)查詢,這樣就完全符合了 RESTful 的無(wú)狀態(tài)規(guī)范。

1.3.4 JWT 存在的問題

說了這么多,JWT 也不是天衣無(wú)縫,由客戶端維護(hù)登錄狀態(tài)帶來(lái)的一些問題在這里依然存在,舉例如下:

  1. 續(xù)簽問題,這是被很多人詬病的問題之一,傳統(tǒng)的 cookie+session 的方案天然的支持續(xù)簽,但是 jwt 由于服務(wù)端不保存用戶狀態(tài),因此很難完美解決續(xù)簽問題,如果引入 redis,雖然可以解決問題,但是 jwt 也變得不倫不類了。
  2. 注銷問題,由于服務(wù)端不再保存用戶信息,所以一般可以通過修改 secret 來(lái)實(shí)現(xiàn)注銷,服務(wù)端 secret 修改后,已經(jīng)頒發(fā)的未過期的 token 就會(huì)認(rèn)證失敗,進(jìn)而實(shí)現(xiàn)注銷,不過畢竟沒有傳統(tǒng)的注銷方便。
  3. 密碼重置,密碼重置后,原本的 token 依然可以訪問系統(tǒng),這時(shí)候也需要強(qiáng)制修改 secret。
  4. 基于第 2 點(diǎn)和第 3 點(diǎn),一般建議不同用戶取不同 secret。

當(dāng)然,為了解決 JWT 存在的問題,也可以將 JWT 結(jié)合 Redis 來(lái)用,服務(wù)端生成的 JWT 字符串存入到 Redis 中并設(shè)置過期時(shí)間,每次校驗(yàn)的時(shí)候,先看 Redis 中是否存在該 JWT 字符串,如果存在就進(jìn)行后續(xù)的校驗(yàn)。但是這種方式有點(diǎn)不倫不類(又成了有狀態(tài)了)。

2. 實(shí)踐

我們來(lái)看下 gRPC 如何結(jié)合 JWT。

2.1 項(xiàng)目創(chuàng)建

首先我先給大家看下我的項(xiàng)目結(jié)構(gòu):

├── grpc_api
│   ├── pom.xml
│   └── src
├── grpc_client
│   ├── pom.xml
│   └── src
├── grpc_server
│   ├── pom.xml
│   └── src
└── pom.xml

還是跟之前文章中的一樣,三個(gè)模塊,grpc_api 用來(lái)存放一些公共的代碼。

grpc_server 用來(lái)放服務(wù)端的代碼,我這里服務(wù)端主要提供了兩個(gè)接口:

  1. 登錄接口,登錄成功之后返回 JWT 字符串。
  2. hello 接口,客戶端拿著 JWT 字符串來(lái)訪問 hello 接口。

grpc_client 則是我的客戶端代碼。

2.2 grpc_api

我將 protocol buffers 和一些依賴都放在 grpc_api 模塊中,因?yàn)閷?lái)我的 grpc_server 和 grpc_client 都將依賴 grpc_api。

我們來(lái)看下這里需要的依賴和插件:

<dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty-shaded</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>annotations-api</artifactId>
        <version>6.0.53</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.6.2</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.51.0:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

這里的依賴和插件松哥在本系列的第一篇文章中都已經(jīng)介紹過了,唯一不同的是,這里引入了 JWT 插件,JWT 我使用了比較流行的 JJWT 這個(gè)工具。JJWT 松哥在之前的文章和視頻中也都有介紹過,這里就不再啰嗦了。

先來(lái)看看我的 Protocol Buffers 文件:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.javaboy.grpc.api";
option java_outer_classname = "LoginProto";
import "google/protobuf/wrappers.proto";

package login;

service LoginService {
  rpc login (LoginBody) returns (LoginResponse);
}

service HelloService{
  rpc sayHello(google.protobuf.StringValue) returns (google.protobuf.StringValue);
}

message LoginBody {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  string token = 1;
}

經(jīng)過前面幾篇文章的介紹,這里我就不多說啦,就是定義了兩個(gè)服務(wù):

  • LoginService:這個(gè)登錄服務(wù),傳入用戶名密碼,返回登錄成功之后的令牌。
  • HelloService:這個(gè)就是一個(gè)打招呼的服務(wù),傳入字符串,返回也是字符串。

定義完成之后,生成對(duì)應(yīng)的代碼即可。

接下來(lái)再定義一個(gè)常量類供 grpc_server 和 grcp_client 使用,如下:

public interface AuthConstant {
    SecretKey JWT_KEY = Keys.hmacShaKeyFor("hello_javaboy_hello_javaboy_hello_javaboy_hello_javaboy_".getBytes());
    Context.Key<String> AUTH_CLIENT_ID = Context.key("clientId");
    String AUTH_HEADER = "Authorization";
    String AUTH_TOKEN_TYPE = "Bearer";
}

這里的每個(gè)常量我都給大家解釋下:

  1. JWT_KEY:這個(gè)是生成 JWT 字符串以及進(jìn)行 JWT 字符串校驗(yàn)的密鑰。
  2. AUTH_CLIENT_ID:這個(gè)是客戶端的 ID,即客戶端發(fā)送來(lái)的請(qǐng)求攜帶了 JWT 字符串,通過 JWT 字符串確認(rèn)了用戶身份,就存在這個(gè)變量中。
  3. AUTH_HEADER:這個(gè)是攜帶 JWT 字符串的請(qǐng)求頭的 KEY。
  4. AUTH_TOKEN_TYPE:這個(gè)是攜帶 JWT 字符串的請(qǐng)求頭的參數(shù)前綴,通過這個(gè)可以確認(rèn)參數(shù)的類型,常見取值有 Bearer 和 Basic。

如此,我們的 gRPC_api 就定義好了。

2.3 grpc_server

接下來(lái)我們來(lái)定義 gRPC_server。

首先來(lái)定義登錄服務(wù):

public class LoginServiceImpl extends LoginServiceGrpc.LoginServiceImplBase {
    @Override
    public void login(LoginBody request, StreamObserver<LoginResponse> responseObserver) {
        String username = request.getUsername();
        String password = request.getPassword();
        if ("javaboy".equals(username) && "123".equals(password)) {
            System.out.println("login success");
            //登錄成功
            String jwtToken = Jwts.builder().setSubject(username).signWith(AuthConstant.JWT_KEY).compact();
            responseObserver.onNext(LoginResponse.newBuilder().setToken(jwtToken).build());
            responseObserver.onCompleted();
        }else{
            System.out.println("login error");
            //登錄失敗
            responseObserver.onNext(LoginResponse.newBuilder().setToken("login error").build());
            responseObserver.onCompleted();
        }
    }
}

省事起見,我這里沒有連接數(shù)據(jù)庫(kù),用戶名和密碼固定為 javaboy 和 123。

登錄成功之后,就生成一個(gè) JWT 字符串返回。

登錄失敗,就返回一個(gè) login error 字符串。

再來(lái)看我們的 HelloService 服務(wù),如下:

public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void sayHello(StringValue request, StreamObserver<StringValue> responseObserver) {
        String clientId = AuthConstant.AUTH_CLIENT_ID.get();
        responseObserver.onNext(StringValue.newBuilder().setValue(clientId + " say hello:" + request.getValue()).build());
        responseObserver.onCompleted();
    }
}

這個(gè)服務(wù)就更簡(jiǎn)單了,不啰嗦。唯一值得說的是 AuthConstant.AUTH_CLIENT_ID.get(); 表示獲取當(dāng)前訪問用戶的 ID,這個(gè)用戶 ID 是在攔截器中存入進(jìn)來(lái)的。

最后,我們來(lái)看服務(wù)端比較重要的攔截器,我們要在攔截器中從請(qǐng)求頭中獲取到 JWT 令牌并解析,如下:

public class AuthInterceptor implements ServerInterceptor {
    private JwtParser parser = Jwts.parser().setSigningKey(AuthConstant.JWT_KEY);

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
        String authorization = metadata.get(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER));
        Status status = Status.OK;
        if (authorization == null) {
            status = Status.UNAUTHENTICATED.withDescription("miss authentication token");
        } else if (!authorization.startsWith(AuthConstant.AUTH_TOKEN_TYPE)) {
            status = Status.UNAUTHENTICATED.withDescription("unknown token type");
        } else {
            Jws<Claims> claims = null;
            String token = authorization.substring(AuthConstant.AUTH_TOKEN_TYPE.length()).trim();
            try {
                claims = parser.parseClaimsJws(token);
            } catch (JwtException e) {
                status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);
            }
            if (claims != null) {
                Context ctx = Context.current()
                        .withValue(AuthConstant.AUTH_CLIENT_ID, claims.getBody().getSubject());
                return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
            }
        }
        serverCall.close(status, new Metadata());
        return new ServerCall.Listener<ReqT>() {
        };
    }
}

這段代碼邏輯應(yīng)該好理解:

  1. 首先從 Metadata 中提取出當(dāng)前請(qǐng)求所攜帶的 JWT 字符串(相當(dāng)于從請(qǐng)求頭中提取出來(lái))。
  2. 如果第一步提取到的值為 null 或者這個(gè)值不是以指定字符 Bearer 開始的,說明這個(gè)令牌是一個(gè)非法令牌,設(shè)置對(duì)應(yīng)的響應(yīng) status 即可。
  3. 如果令牌都沒有問題的話,接下來(lái)就進(jìn)行令牌的校驗(yàn),校驗(yàn)失敗,則設(shè)置相應(yīng)的 status 即可。
  4. 校驗(yàn)成功的話,我們就會(huì)獲取到一個(gè) Jws<Claims> 對(duì)象,從這個(gè)對(duì)象中我們可以提取出來(lái)用戶名,并存入到 Context 中,將來(lái)我們?cè)?HelloServiceImpl 中就可以獲取到這里的用戶名了。
  5. 最后,登錄成功的話,Contexts.interceptCall 方法構(gòu)建監(jiān)聽器并返回;登錄失敗,則構(gòu)建一個(gè)空的監(jiān)聽器返回。

最后,我們?cè)賮?lái)看看啟動(dòng)服務(wù)端:

public class LoginServer {
    Server server;

    public static void main(String[] args) throws IOException, InterruptedException {
        LoginServer server = new LoginServer();
        server.start();
        server.blockUntilShutdown();
    }

    public void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new LoginServiceImpl())
                .addService(ServerInterceptors.intercept(new HelloServiceImpl(), new AuthInterceptor()))
                .build()
                .start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            LoginServer.this.stop();
        }));
    }

    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
}

這個(gè)跟之前的相比就多加了一個(gè) Service,添加 HelloServiceImpl 服務(wù)的時(shí)候,多加了一個(gè)攔截器,換言之,登錄的時(shí)候,請(qǐng)求是不會(huì)被這個(gè)認(rèn)證攔截器攔截的。

好啦,這樣我們的 grpc_server 就開發(fā)完成了。

2.4 grpc_client

接下來(lái)我們來(lái)看 grpc_client。

先來(lái)看登錄:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);
        login(stub);
    }

    private static void login(LoginServiceGrpc.LoginServiceStub stub) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build(), new StreamObserver<LoginResponse>() {
            @Override
            public void onNext(LoginResponse loginResponse) {
                System.out.println("loginResponse.getToken() = " + loginResponse.getToken());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

這個(gè)方法直接調(diào)用就行了,看過前面幾篇 gRPC 文章的話,這里都很好理解。

再來(lái)看 hello 接口的調(diào)用,這個(gè)接口調(diào)用需要攜帶 JWT 字符串,而攜帶 JWT 字符串,則需要我們構(gòu)建一個(gè) CallCredentials 對(duì)象,如下:

public class JwtCredential extends CallCredentials {
    private String subject;

    public JwtCredential(String subject) {
        this.subject = subject;
    }

    @Override
    public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) {
        executor.execute(() -> {
            try {
                Metadata headers = new Metadata();
                headers.put(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER),
                        String.format("%s %s", AuthConstant.AUTH_TOKEN_TYPE, subject));
                metadataApplier.apply(headers);
            } catch (Throwable e) {
                metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));
            }
        });
    }

    @Override
    public void thisUsesUnstableApi() {

    }
}

這里就是將請(qǐng)求的 JWT 令牌放入到請(qǐng)求頭中即可。

最后來(lái)看看調(diào)用:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);
        sayHello(channel);
    }

    private static void sayHello(ManagedChannel channel) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        HelloServiceGrpc.HelloServiceStub helloServiceStub = HelloServiceGrpc.newStub(channel);
        helloServiceStub
                .withCallCredentials(new JwtCredential("eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJqYXZhYm95In0.IMMp7oh1dl_trUn7sn8qiv9GtO-COQyCGDz_Yy8VI4fIqUcRfwQddP45IoxNovxL"))
                .sayHello(StringValue.newBuilder().setValue("wangwu").build(), new StreamObserver<StringValue>() {
            @Override
            public void onNext(StringValue stringValue) {
                System.out.println("stringValue.getValue() = " + stringValue.getValue());
            }

            @Override
            public void onError(Throwable throwable) {
                System.out.println("throwable.getMessage() = " + throwable.getMessage());
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

這里的登錄令牌就是前面調(diào)用 login 方法時(shí)獲取到的令牌。

好啦,大功告成。

3. 小結(jié)

上面的登錄與校驗(yàn)只是松哥給小伙伴們展示的一個(gè)具體案例而已,在此案例基礎(chǔ)之上,我們還可以擴(kuò)展出來(lái)更多寫法,但是萬(wàn)變不離其宗,其他玩法就需要小伙伴們自行探索啦~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容