Keycloak 13 自定義用戶身份認證流程(User Storage SPI)

Keycloak

版本:13.0.0

spring-boot 項目 Github
user-storage-spi 項目 Github

介紹

Keycloak 是爲現代應用程序和服務提供的一個開源的身份和訪問管理的解決方案。

Keycloak 在測試環境可以使用內嵌數據庫,生產環境需要重新配置數據庫。以下將一一介紹如何使用內嵌數據庫、重新配置數據庫。

特別需要注意 Keycloak 是在 WildFly 上構建的。

安裝

系統要求

  • Java8 JDK
  • 至少 512M 內存
  • 至少 1G 磁盤
  • 如果要設置 Keycloak 集羣則需要數據庫,比如:PostgreSQL、Oracle、MySQL 等
  • 如果要運行集羣,需要網絡支持廣播。當然也可以不需要,只不過需要更改一堆配置

目錄結構

  • bin/ —— 各種啓動服務、服務器上執行管理的腳本
  • domain/ —— 集羣模式下的配置文件和工作目錄
  • modules/ —— 服務使用的 Java 包
  • standalone/ —— 單機模式下的配置文件和工作目錄
  • standalone/deployments/ —— 你自定義的擴展文件
  • themes/ —— 界面主題文件

使用 Docker 安裝 Keycloak

  1. 啓動容器

    docker run -p 10010:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:13.0.0
    
  2. 成功啓動後訪問:http://127.0.0.1:10010/

  3. 登錄 Keycloak 服務

    a. 點擊 Administration Console

    b. 輸入賬號:admin,密碼:admin

    c. 進入控制檯界面

使用

創建 realm 和 用戶

創建一個 realm 和用戶以訪問內置的賬戶管理控制檯。把 realm 想像成租戶的概念。

  • Master realm —— 這個 realm 是初始化時創建的,它包含超級管理員。使用這個 realm 管理其它的 realm。
  • Other realm —— 超級管理員創建的 realm 。在這些 realm 裏,超級管理員創建用戶和應用程序,用戶擁有應用程序。

創建 realm

  1. 點擊 Add realm 按鈕

  2. 輸入 realm 名稱

  3. 點擊 create 按鈕完成創建
    點擊創建完成 demo realm 創建。

創建用戶

  1. 切換 realm 到 Demo

  2. 創建用戶,Users -> Add User,輸入 Username ,點擊 Save 按鈕

  3. 設置密碼

新建用戶登錄控制檯

  1. 退出 admin 賬戶登錄
  2. 地址輸入:http://127.0.0.1:10010/auth/realms/demo/account/,點擊 Sign In 按鈕,使用新創建的用戶 Zhang 登錄
  3. 設置新密碼
  4. 登錄成功

Spring-Boot 認證

註冊客戶端到 Keycloak 中

應用程序或服務爲了能使用 Keycloak,必須在 Keycloak 中註冊一個客戶端。可以通過超級管理員界面註冊,客戶端也可以自己通過 Keycloak 服務註冊。

客戶端註冊服務提供內置支持:Keycloak Client Representations、OIDC 客戶端元數據、SAML 實體描述。客戶端註冊服務地址是:/auth/realms/<realm>/clients-registrations/<provider>

內置的 provider

  • default —— Keycloak Client Representation(JSON)
  • install —— Keycloak Adapter Configuration(JSON)
  • openid-connect —— OIDC 客戶端元數據描述(JSON)
  • saml2-entity-descriptor —— SAML 實體描述者(XML)

認證

調用客戶端註冊服務需要令牌。令牌可以是 bearer 令牌,初始化訪問令牌或者註冊令牌。不需要令牌註冊客戶端也可以,但是需要配置客戶端註冊策略。

Bearer 令牌

Bearer 令牌可以代表用戶或者服務賬戶。調用端點需要以下權限:

  • create-client 或者 manage-client —— 創建客戶端
  • view-client 或者 manage-client —— 查看客戶端
  • manage-client —— 更新或者刪除客戶端

如果使用 Bearer 令牌創建客戶端,推薦使用來自服務賬戶(create-client 角色)的令牌。

初始化訪問令牌

推薦使用初始化訪問令牌註冊客戶端。初始化訪問令牌只能用於創建客戶端,並且可以配置有效期,同時可以配置可以創建多少客戶端。

初始化訪問令牌可以通過超級管理員控制檯創建。

點擊保存,完成令牌的創建。

當點擊保存以後會生成令牌,這個令牌如果忘記複製了,那麼就只能重新創建了。使用 bearer 令牌:

Authorization: bearer eyJhbGciOiJSUz...
註冊訪問令牌

當通過客戶端註冊服務創建客戶端時,返回值會包含一個註冊訪問令牌。註冊訪問令牌提供檢索客戶端配置、更新或者刪除客戶端的權限。註冊訪問令牌使用方式和 bear 令牌、初始化訪問令牌的使用方式是一樣的。註冊訪問令牌是一次性的,當它使用時,返回值裏總會包含一個新的令牌。

如果客戶端在客戶端註冊服務之外創建,註冊訪問令牌就不會和客戶端關聯起來了。但可以在超級管理員控制檯生成註冊訪問令牌。

Keycloak Representations

default 客戶端註冊提供商可以創建、檢索、更新、刪除客戶端。使用 Keycloak Client Representation 轉換提供配置客戶端支持,就像在超級管理員控制檯配置的一樣。

創建 Client Representation(JSON)執行 HTTP POST 請求 /auth/realms/<realm>/clients-registrations/default

檢索 Client Representation 執行 GET 請求/auth/realms/<realm>/clients-registrations/default/<client id>

它也會返回新的註冊訪問令牌。

要更新 Client Representation 執行 HTTP PUT 請求 /auth/realms/<realm>/clients-registrations/default/<client id>

它也會返回一個新的註冊訪問令牌。

要刪除 Client Representation 執行 HTTP DELETE 請求 /auth/realms/<realm>/clients-registrations/default/<client id>

Keycloak 適配器配置

installation 客戶端註冊提供商可以用於爲客戶端獲取適配器配置。除了令牌身份驗證之外,還可以使用 HTTP basic 認證(通過客戶端憑證)。使用下列請求頭以完成 HTTP basic 認證:

Authorization: basic BASE64(client-id + ':' + client-secret)

要獲取適配器配置執行 HTTP GET 請求:/auth/realms/<realm>/clients-registrations/install/<client id>

公共客戶端不需要身份認證。這意味着 JavaScript 適配器可以通過以上 URL 直接從 Keycloak 加載客戶端配置。

OIDC 動態客戶端註冊

終端在 Keycloak 中註冊客戶端 /auth/realms/<realm>/clients-registrations/openid-connect[/<client id>]

在 OIDC 發現中可以爲 realm 找到終端,/auth/realms/<realm>/.well-known/openid-configuration

客戶端註冊策略

Keycloak 當前支持兩種方式註冊客戶端(通過客戶端註冊服務)。

  • 認證請求 —— 註冊客戶端請求要麼包含初始化訪問令牌,要麼包含 Bearer 令牌
  • 匿名請求 —— 註冊客戶端不需要包含任何令牌

匿名客戶端註冊請求是非常有趣和強大的功能,任何人都可以註冊客戶端並且沒有限制。因此提出了客戶端註冊策略 SPI,它提供了一個限制的方式(誰能註冊,在什麼條件下)。

在 Keycloak 超級管理員控制檯中,你可以看到匿名請求策略配置和認證請求策略配置。

當前支持的策略:

  • Trusted Hosts Policy —— 可以配置信任的 host 和域名。默認的,沒有白名單 host,所以匿名客戶端註冊實際上是禁用的。
  • Consent Required Policy —— 新註冊的客戶端 Consent Allowed 開關是啓動的。所以身份認證成功後,用戶將看到准許會話(如果需要的話)。
  • Protocol Mapper Policy —— 允許配置協議映射實現白名單。
  • Client Scope Policy —— 允許 Client Scopes 白名單,用於新註冊的客戶端或者更新的客戶端。
  • Full Scope Policy —— 新註冊的客戶端 Full Scope Allowed開關是關閉的。意味這這些客戶端沒有任何 realm 角色或者客戶端角色。
  • Max Clients Policy —— 如果註冊的客戶端數量在 realm 中大於或等於設定值將被駁回。默認值 200。
  • Client Disabled Policy —— 新註冊客戶端是禁用的。意味着超級管理員需要手動通過和啓用新註冊的客戶端。這個策略默認不啓用。

管理客戶端

客戶端是用戶請求身份認證的實體。客戶端有兩種格式。第一種是單點登錄的(SSO)。另一種是類型是獲取訪問令牌然後代表用戶訪問服務。

OIDC 客戶端

創建 OIDC 客戶端
  1. 客戶端列表

  2. 添加客戶端,Client ID 是客戶端的身份標識。下一步選擇客戶端協議 openid-connect

    Client ID
    數據字母字符串,用於客戶端身份識別(當 OIDC 請求時)。

    Name
    客戶端名稱。

    Description
    客戶端描述。

    Enabled
    如果關閉,客戶端將不允許請求驗證。

    Consent Required
    如果打開,用戶將得到一個准許頁面(用於詢問用戶是否授權應用程序訪問)。頁面同時顯示客戶端要訪問的元數據信息,用戶可以看到客戶端要訪問的信息。

    Access Type
    OIDC 客戶端類型。

    • confidential:機密訪問類型用於服務端客戶端(需要執行瀏覽器登錄和需要客戶端密碼)。這個類型用於服務端應用程序。
    • public:Public 訪問類型是客戶端類型客戶端(需要執行瀏覽器登錄)。客戶端類型應用程序沒有安全保存祕密的方式。相反,通過爲客戶端配置正確的重定向 URI 來限制訪問非常重要。
    • bearer-only:Bearer-only 訪問類型意味着應用程序僅允許 bearer 令牌請求。如果打開這個,應用程序不能參與瀏覽器登錄。

    Standard Flow Enabled
    如果打開這個,客戶端將使用 OIDC 授權碼工作流。

    Implicit Flow Enabled
    如果打開這個,客戶端將使用 OIDC 隱式工作流。

    Direct Access Grants Enabled
    如果打開這個,客戶端將使用 OIDC 直接訪問授權。

    OAuth 2.0 Device Authorization Grant Enabled
    如果打開這個,客戶端將使用 OIDC 設備授權許可。

    OpenID Connect Client Initiated Backchannel Authentication Grant Enabled
    如果打開這個,客戶端將使用 OIDC 客戶端初始化後端渠道認證許可。

    Root URL
    如果 Keycloak 不使用任何相對 URL,這個值就是預留值。

    Valid Redirect URIs
    這個是必填字段。輸入 URL 模版然後點擊 + 號添加。點擊 - 號移除 URL。記住,最後還要點擊 Sava 按鈕。通配符 * 只能用於 URI 的末端,例如:http://host.com/*

    註冊重定向 URL 模版時,你應該考慮避免被攻擊。

    Base URL
    如果 Keycloak 要鏈接客戶端,這個值需要設置。

    Admin URL
    爲 Keycloak 指定客戶端適配器,這個值是客戶端回調終端。Keycloak 服務將使用這個 URI 回調(比如:推送取消策略、執行後端渠道退出登錄、其它超級管理員操作)。對於 Keycloak servlet 適配器來說,這個值是 servlet 應用程序的 root URL。

    Web Origins
    這個設置是以 CORS 爲中心。

  3. 保密客戶端證書
    如果客戶端 access type 設置爲 confidential 時,頁面將會顯示 Credentials 標籤。注意,選擇 Confidential 標籤要保存以後纔會能看到 Credentials標籤。

    Client Authenticator 下拉框指定你的加密客戶端證書類型。默認是 Client Id and Secret。secret 自動生成,並且 Regenerate Secret 按鈕可以重新生成 secret。

    此外,可以選擇 Signed Jwt 或者 X509 Certificate 驗證代替 secret。

    Signed JWT

    當選擇 Signed Jwt 類型時,你需要爲客戶端生成私鑰和證書。私鑰用於 JWT 簽名,證書用於服務端驗證簽名。點擊 Generate new keys and certificate 按鈕生成私鑰和證書。

    也可以使用其它工具生成,然後導入。

  4. 新建 Spring-boot 項目

  5. 創建 Keycloak 客戶端,導航到 http://127.0.0.1:10010/,切換到 Demo realm ,點擊 Clients 菜單,點擊 Create 按鈕,創建一個 Demo realm 下的客戶端。客戶端 ID 爲:spring-boot-toy。選擇 Access Typeconfidential

  6. 創建角色 toy-admin,並給用戶賦予角色


  7. Spring 項目添加 Maven 引用

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
         <modelVersion>4.0.0</modelVersion>
         <parent>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-parent</artifactId>
             <version>2.3.10.RELEASE</version>
             <relativePath/> <!-- lookup parent from repository -->
         </parent>
         <groupId>com.toy.keycloak</groupId>
         <artifactId>toy-keycloak</artifactId>
         <version>0.0.1-SNAPSHOT</version>
         <name>toy-keycloak</name>
    
         <properties>
             <java.version>1.8</java.version>
         </properties>
         <dependencies>
             <dependency>
                 <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-spring-boot-starter</artifactId>
             </dependency>
    
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-devtools</artifactId>
                 <scope>runtime</scope>
                 <optional>true</optional>
             </dependency>
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
                 <optional>true</optional>
             </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-test</artifactId>
                 <scope>test</scope>
                 <exclusions>
                     <exclusion>
                         <groupId>org.junit.vintage</groupId>
                         <artifactId>junit-vintage-engine</artifactId>
                     </exclusion>
                 </exclusions>
             </dependency>
         </dependencies>
    
         <dependencyManagement>
             <dependencies>
                 <dependency>
                     <groupId>org.keycloak.bom</groupId>
                     <artifactId>keycloak-adapter-bom</artifactId>
                     <version>13.0.0</version>
                     <type>pom</type>
                     <scope>import</scope>
                 </dependency>
             </dependencies>
         </dependencyManagement>
    
         <build>
             <plugins>
                 <plugin>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-maven-plugin</artifactId>
                     <configuration>
                         <excludes>
                             <exclude>
                                 <groupId>org.projectlombok</groupId>
                                 <artifactId>lombok</artifactId>
                             </exclude>
                         </excludes>
                     </configuration>
                 </plugin>
             </plugins>
         </build>
    
     </project>
    
    
  8. 暴露 Api 接口

     package com.toy.keycloak.webapi;
    
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.bind.annotation.RestController;
    
     /**
     * @author Zhang_Xiang
     * @since 2021/5/11 16:56:57
     */
     @RestController
     @RequestMapping("temp")
     public class TempController {
    
         @GetMapping("weather")
         public String weather(){
             return "晴天☀️";
         }
    
     }
    
    
  9. 配置

    使用 Tomcat、Undertow、Jetty 不需要額外配置。在 application.properties 中配置 Keycloak 如下:

     keycloak.realm=demo  # realm 名稱
     keycloak.auth-server-url=http://127.0.0.1:10010/auth   # Keycloak 基礎服務地址
     keycloak.ssl-required=external
     keycloak.resource=spring-boot-toy   # 應用程序客戶端 ID
     keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
     keycloak.use-resource-role-mappings=true
     keycloak.securityConstraints[0].authRoles[0]=toy-admin
     keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
    

    設置 keycloak.enabled = false 可以停用 Keycloak 。

  10. 啓動 Spring 應用程序,並訪問 http://localhost:8080/temp/weather

    輸入用戶名 zhang,密碼123456

  11. 管理登錄用戶會話,用戶登錄後,進入 Keycloak 管理後臺,切換到 Demo realm 可以看到目前登錄的會話列表。

    點擊 Logout all 按鈕,所有用戶都將退出登錄。

使用外部數據庫

Keycloak 內嵌了 H2 內存數據庫。Keycloak 默認使用 H2 持久化數據。H2 數據庫不適用於高併發場景並且不適用於集羣。

Keycloak 使用兩層技術持久化關係數據。底層技術是 JDBC。JDBC 用於連接 RDBMS。每個數據庫提供商都有不同的 JDBC 驅動。頂層技術用於持久化的是 Hibernate JPA。

用戶存儲 SPI

使用用戶存儲 SPI 擴展 Keycloak 以連接外部用戶數據和證書存儲。當 Keycloak 運行時查找用戶時,比如用戶登錄,Keycloak 執行幾個步驟定位用戶。首先看用戶是在在用戶緩存中,然後在本地數據庫中查找,如果找不到,循環用戶存儲 SPI 執行查找。

用戶存儲 SPI 提供商實現打包和部署和 Java EE 組件相似。默認不啓用組件,如果要啓用需要在 Keycloak 超級管理員控制檯界面中配置 User Feberation

打包和部署

用戶存儲提供商打包爲 JAR 然後部署到 Keycloak 運行時或者從 Keycloak 運行時取消部署,就像 WildFly 應用程序服務部署服務一樣。你也可以直接拷貝 JAR 包到 standalone/deployments/ 服務器目錄,或者使用 JBoss CLI 執行部署。

爲了 Keycloak 能識別服務提供商,你需要添加一個文件到 JAR 包中:META-INF/services/org.keycloak.storage.UserStorageProviderFactory。這個文件必須包含實現 UserStorageProviderFactory 類的全路徑:

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

自定義用戶存儲 Provider

使用已有用戶數據庫。

MysqlUserStorageProvider 實現了很多接口,實現 UserStorageProvider 是實現是 SPI 的基本要求,換句話說,不實現 UserStorageProvider 就不能實現 SPI,UserLookupProvider 用於實現從外部數據庫查找用戶以實現用戶登錄,UserQueryProvider 定義複雜查詢以查找用戶,CredentialInputValidator 驗證不同的證書類型(比如:驗證登錄密碼)。

  1. provider

    public class MysqlUserStorageProvider implements UserLookupProvider, UserQueryProvider, CredentialInputValidator, UserStorageProvider {
    
        protected KeycloakSession session;
        protected ComponentModel model;
    
        public MysqlUserStorageProvider(KeycloakSession session, ComponentModel model) {
            this.session = session;
            this.model = model;
        }
    
        ...
    }
    
    
  2. ProviderFactory,這裏的 ProviderConfigProperty 用於自定義標籤,換句話說,在這裏定義了標籤以後,可以在 Keycloak 管理界面設置值,代碼可以直接讀取到這些值。添加自定義 SPI 時,要留意添加在什麼 realm 下。

    public class MysqlUserStorageProviderFactory implements UserStorageProviderFactory<MysqlUserStorageProvider> {
    
    protected final List<ProviderConfigProperty> configMetadata;
    
    public MysqlUserStorageProviderFactory() {
        configMetadata = ProviderConfigurationBuilder.create()
                .property()
                .name(CONFIG_KEY_JDBC_DRIVER)
                .label("JDBC Driver Class")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("org.h2.Driver")
                .helpText("Fully qualified class name of the JDBC driver")
                .add()
                .property()
                .name(CONFIG_KEY_JDBC_URL)
                .label("JDBC URL")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("jdbc:h2:mem:customdb")
                .helpText("JDBC URL used to connect to the user database")
                .add()
                .property()
                .name(CONFIG_KEY_DB_USERNAME)
                .label("Database User")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("Username used to connect to the database")
                .add()
                .property()
                .name(CONFIG_KEY_DB_PASSWORD)
                .label("Database Password")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("Password used to connect to the database")
                .secret(true)
                .add()
                .property()
                .name(CONFIG_KEY_VALIDATION_QUERY)
                .label("SQL Validation Query")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("SQL query used to validate a connection")
                .defaultValue("select 1")
                .add()
                .build();
    }
    
    @Override
    public MysqlUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new MysqlUserStorageProvider(session, model);
    }
    
    @Override
    public String getId() {
        return "user-center-provider";
    }
    
    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }
    
    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
        try (Connection c = DbUtil.getConnection(config)) {
            System.out.println(config.get(CONFIG_KEY_VALIDATION_QUERY));
            c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            throw new ComponentValidationException("Unable to validate database connection", ex);
        }
    }
    }
    


  3. 查看用戶列表

Spring-boot 使用外部用戶認證

  1. application.properties

     keycloak.realm=demo
     keycloak.auth-server-url=http://127.0.0.1:10010/auth
     keycloak.ssl-required=external
     keycloak.resource=spring-boot-toy
     keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
     keycloak.use-resource-role-mappings=true
     keycloak.verify-token-audience=true
     keycloak.securityConstraints[0].authRoles[0]=user
     keycloak.security-constraints[0].securityCollections[0].patterns[0]=/temp/weather
    
    • resource:客戶端 ID
    • use-resource-role-mappings:當設置爲 true 時,OIDC Java 適配器將 Token 中查找應用程序級用戶角色映射。設置爲 false 時,將從 realm 裏查找用戶角色映射。默認設置爲 false。
    • ssl-required:確保所有和 Keycloak 通訊的請求是 HTTPS,生產環境應設置爲 all,默認值是 external,即外部請求需要 HTTPS,可選值是:allexternalnone
    • verify-token-audience:設置爲 true 時,Bearer Token 進行身份認證時,適配器會驗證令牌是否包含客戶端名稱。啓用將改善安全性(推薦)。

Keycloak 角色

一個 Realm 有多個客戶端,一個客戶端有多個用戶。在 Keycloak 有3種角色:

  • Realm Role:全局角色,屬於指定 realm。任何客戶端都可以訪問這個角色,並且把這個角色映射到任何用戶。
  • Client Role:屬於指定客戶端的角色。只能映射到該客戶端下的用戶。
  • Composite Role:多個角色的組合。

在客戶端設置中啓用 Service Accounts Enabled,在 demo Realm 中添加 ordinary_user 角色。

Service Account Roles 標籤中添加 ordinary_user 角色。

添加該角色以後,切換到 User 標籤,可以看到用戶中已經關聯了角色。

使用數據庫用戶登錄

  1. 添加角色限制 ordinary_user

    keycloak.realm=demo
    keycloak.auth-server-url=http://127.0.0.1:10010/auth
    keycloak.ssl-required=external
    keycloak.resource=spring-boot-toy
    keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
    keycloak.use-resource-role-mappings=true
    keycloak.verify-token-audience=true
    keycloak.securityConstraints[0].authRoles[0]=toy-admin
    keycloak.security-constraints[0].auth-roles[1]=ordinary_user
    keycloak.security-constraints[0].securityCollections[0].patterns[0]=/temp/weather
    
  2. 啓動服務,訪問 localhost:8080/temp/weather,輸入自定義數據庫中的用戶、密碼,以訪問接口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章