Spring之旅第十二站:Spring Security 數據存儲、攔截請求 、認證用戶、*、

Spring Security

Spring Security 是基於Spring 應用程序提供的聲明式安全保護的安全框架。Spring Sercurity 提供了完整的安全性解決方案,它可以在Web請求級別和方法調用級別處理身份認證和受權,由於是基於Spring,因此Spring Security充分利用了依賴注入(Dependency injection DI) 和麪向切面的技術。css

Spring Security從兩個角度來解決安全性,他使用Servlet規範中的Filter保護Web請求並限制URL級別的訪問。Spring Security還可以使用AOP保護方法調用——藉助於對象代理和使用通知,可以取保只有具有適當權限的用戶才能訪問安全保護的方法。html

說明

若是你有幸能看到。後面的章節暫時不更新了,改變學習方式了。重要理解思想,這本書寫的太好了。記得要看做者的代碼,書上只是闡述了知識點。還有之後會把重點放在GitHub上,閱讀別人的代碼,本身理解的同時在模仿出來,分享給你們。大家的點贊就是對個人支持,謝謝你們了。前端

  • 一、本文參考了《Spring 實戰》重點內容,參考了做者GitHub上的代碼,推薦使用chrome上的GitHub插件Insight.io,FireFox也有。
  • 二、本文只爲記錄做爲之後參考,要想真正領悟Spring的強大,請看原書。跟着做者套路來,先別瞎搗騰!!!
  • 三、在一次佩服老外,國外翻譯過來的書,在GiuHub上大都有實例。看書的時候,跟着敲一遍,效果很好。
  • 四、代碼和筆記在這裏GitHub,對你有幫助的話,歡迎點贊。
  • 五、每一個人的學習方式不同,找到合適本身的就行。2018,加油。
  • 六、Java 8 In Action 的做者Mario Fusco
  • 七、Spring In Action 、Spring Boot In Action的做者Craig Walls
  • 八、知其然,也要知其因此然。有些是在Atom上手敲的,有拼寫錯誤,還請見諒。
  • 九、Spring Web Flow 在項目中用的多嗎??感受不錯的樣子,就不發筆記了.

談一些我的感覺java

  • 一、趕快學習Spring吧,Spring MVC 、Spring Boot 、微服務。
  • 二、重點中的重點,學習JDK 8 Lambda,Stream,Spring 5 最低要求JDK1.8.
  • 三、還有Netty、放棄SH吧,否則你會落伍的。
  • 四、多看一些國外翻譯過來的書,例如 Xxx In Action 系列。權威指南系列。用Kindle~
  • 五、寫代碼以前先寫測試,這就是老外不一樣之處。學到了不少技巧。
  • 六、再一次佩服老外對細節的處理。值得咱們每個人學習

一、理解Spring Security的模塊

將Spring Security模塊添加到應用程序的類路徑下。應用程序的類路徑下至少包含core和Configuration這兩個模塊。它常常被用於保護Web應用,添加Web模塊,同時還須要JSP標籤庫。git

二、過濾Web請求

Spring Security藉助一系列Servlet Filter來提供各類安全性功能。github

DelegatingFilterProxy是一個特殊的ServletFilter,它自己所做的工做並很少,只是將工做委託給一個Javax.servlet.Filter實現類,這個實現類做爲一個<bean<>註冊在Spring上下文中。web

web.xml配置算法

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterproxy</filter-class>
</filter>

DelegatingFilterproxy會將過濾邏輯委託給它。spring

若是你但願藉助於WebApplicationInitializer以JavaConfig配置,須要一個擴展類chrome

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

/**
 * Created by guo on 2/26/2018.
 */
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}

AbstractSecurityWebApplicationInitializer實現了WebapplicationInitializer,所以Spirng會發現他,並用它在Web容器中註冊DelegatingFilterproxy.它不須要重載任何方法。它會攔截髮往應用中的請求,並將其委託給springSecurityFilterChain

Spting Security依賴一系列ServletFilter來提供不一樣的安全特性。可是你不須要細節。當咱們啓用Web安全性的時候,會自動建立這些Filter。

三、編寫簡單的安全性配置。

Spring 3.2引入了新的java配置方案,徹底不須要經過XML來配置安全性功能了。

@Configuration
@EnableWebSecurity    //啓用Web安全性
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

@EnableWebSecurity啓用Web安全功能,但它自己並無什麼用處 ,Spring Security必須配置在一個實現類WebSecurityConfigurer的bean中。

若是你的應用碰巧是在使用Spirng MVC的話,那麼就應該考慮使用@EnableWebMvcSecurity還能配置一個Spring MVC參數解析器。這樣的話,處理器方法就可以經過帶有@AuthenticationPrincipal註解的參數獲取得認證用戶的principal。它同時還配置一個bean,在使用Spring表單綁定標籤庫來定義表單時,這個bean會自動添加一個隱藏的跨站請求僞造(CSRF)token的輸入流。

@Configuration
@EnableWebMvcSecurity//啓用Web安全性
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().and()
                .httpBasic();
    }
}

這個簡單的默認配置指定了如何保護HTTP請求,以及客戶端認證用戶的方案。經過調用 authorizeRequests()和anyRequest().authenticated()就會要求所欲進入應用的Http都要進行認證,他也配置Spring Securoty支持基於表單的登陸以及HTTP Basic方式的認證。

爲了讓Spring 知足咱們應用的需求,還須要在添加一些配置。

  • 配置用戶儲存
  • 指定哪些請求須要認證,那些請求不須要認證,以及所須要的權限
  • 提供一個自定義的登陸頁面,替代原來簡單的默認登陸頁。

除了Spring Security的這些功能,咱們可能還但願給予安全限制,有選擇性在Web視圖上顯示特定的內容。

4 選擇查詢用戶詳細信息的服務。

咱們所須要的是用戶的存儲,也就是用戶名、密碼以及其餘信息存儲的地方,在進行認證決策的時候,對其進行檢索。

好消息是Spring Security很是靈活,可以給予各類數據庫存儲來認證用戶名,它內置了多種常見的用戶存儲場景,如內存、關係型數據庫,以及LDAP,但咱們也能夠編寫並插入自定義的用戶存儲實現。

藉助於Spring Security的Java配置,咱們可以很容易的配置一個或多個數據庫存儲方案。

五、使用基於內存的用戶存儲

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER","ADMIN");
    }

經過簡單那的調用inMemoryAuthentication就能啓用內存用戶村蘇。可是,咱們還須要一些用戶,不然的話這個沒用戶並無且別。須要調用weithuser爲其存儲添加新的用戶。以及給定用戶授予一個或多個角色權限的reles()方法

對於調式和開發人員來說,基於內存的用戶存儲是頗有用的,但對於生產級別應用來說,這就不是最理想的狀態了。

五、基於數據庫進行認證

用戶數據一般會存儲在關係型數據庫中,並經過JDBC進行訪問。爲了配置Spring Security使用以JDBC爲支撐的用戶存儲,咱們可使用jdbcAuthentication()方法,所需的最少配置。

@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
            .dataSource(dataSource);
}

咱們必需要配置的知識一個DataSource,這樣的話就能 訪問關係型數據庫裏。

儘管默認的最少配置可以讓一切運轉起來,可是,它對咱們的數據庫模式有一些要求。它預期存在某些存儲用戶數據的表。

public static final String DEF_USERS_BY_USERNAME_QUERY =
                "select username,password,enabled " +
                "from users " +
                "where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
                "select username,authority " +
                "from authorities " +
                "where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
                "select g.id, g.group_name, ga.authority " +
                "from groups g, group_members gm, group_authorities ga " +
                "where gm.username = ? " +
                "and g.id = ga.group_id " +
                "and g.id = gm.group_id";

在第一個查詢中,咱們獲取了用戶的用戶名、密碼以及是否啓用的信息,這些信息用來進行用戶認證。接下來查詢查找 了用戶所授予的權限,用來進行鑑權。最後一個查詢中,查找了用戶做爲羣組的成員所授予的權限。

若是你可以在數據庫中定義和填充知足這些查詢的表,那麼基本上就不須要你在作什麼額外的事情了。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
         .dataSource(dataSource)
         .usersByUsernameQuery(
             "select username,password,true" +
             "from Spitter where username=?")
             .authoritiesByUsernameQuery(
             "select username,'ROLE_USER' from Spitter where username=?");
}

在本例中,咱們只重寫了認證和基本權限的查詢語句,可是經過調用groupAuthoritiesByUsername()方法,咱們也可以將羣組權限重寫爲自定義的查詢語句。

爲了解決密碼明文的問題,咱們藉助於passwordEncode()方法指定一個密碼轉碼器(encoder)

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
         .dataSource(dataSource)
         .usersByUsernameQuery(
             "select username,password,true" +
             "from Spitter where username=?")
             .authoritiesByUsernameQuery(
             "select username,'ROLE_USER' from Spitter where username=?")
        .passwordEncoder(new StandardPasswordEncoder("53cd3t"));
}

passwordEncoder()方法能夠接受Spring Security中passwordEncoder接口的任意實現。加密模塊包含了三個這樣的實現

  • StandardPasswordEncoder
  • NoOpPasswordEncoder
  • BCryptPasswordEncoder

上述代碼使用了StandardPasswordEncoder,可是若是內置的實現沒法知足需求時,你能夠提供自定義的實現 ,passwordEncoder接口以下:

package org.springframework.security.crypto.password;
/**
 * Service interface for encoding passwords.
 */
public interface PasswordEncoder {
    /**
     * Encode the raw password.
     */
    String encode(CharSequence rawPassword);
    /**
     * Verify the encoded password obtained from storage matches the submitted raw password after it too is encoded.
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);
}

無論使用哪個密碼轉化器,都須要理解的一點是:數據庫的祕密是永遠不會解碼的,所採起的策略與之相反。用戶在登陸時輸入的密碼會按照相同的算法進行轉碼,而後在於數據庫中已經轉碼過的密碼進行對比,這個對比是在PasswordEncoder的matches()方法中進行的。

六、基於LDAP進行認證

爲了讓Spring Security使用基於LDAP的認證,咱們可使用ldapAuthentication()方法,這個方法相似於jdbcAuthentication()只不過是LDAP版本

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.ldapAuthentication()
         .userSearchBase("(uid={0})")
         .groupSearchFilter("member={0}");
}

配置密碼比對
基於LDAP進行認證的默認策略是進行綁定操做,直接經過LDAP服務器認證用戶,另外一種可選的方式是進行對比,涉及到輸入的 密碼發送到LDAP目錄上,並要求服務器將這個密碼和用戶的密碼進行對比,由於對比是用LDAP服務器內完成的。實際的祕密能保持私密。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.ldapAuthentication()
         .groupSearchBase("on=people")
             .userSearchBase("(uid={0})")
             .groupSearchFilter("member={0}")
             .groupSearchBase("on=groups")
             .groupSearchFilter("member={0}")
             .passwordCompare();
}

若是密碼被保存在不一樣的屬性中,能夠經過passwordAttribute()方法來聲明密碼屬性的名稱

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.ldapAuthentication()
         .groupSearchBase("on=people")
         .userSearchBase("(uid={0})")
         .groupSearchFilter("member={0}")
         .groupSearchBase("on=groups")
         .groupSearchFilter("member={0}")
         .passwordCompare()
         .passwordEncoder(new Md5PasswordEncoder())
         .passwordAttribute("passcode");
}

爲了不這一點咱們能夠經過調用passwordEncoder()方法指定加密策略。本例中使用MD5加密,這須要LDAP服務器上密碼也是MD5進行加密

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.ldapAuthentication()
         .groupSearchBase("on=people")
         .userSearchBase("(uid={0})")
         .groupSearchFilter("member={0}")
         .groupSearchBase("on=groups")
         .groupSearchFilter("member={0}")
         .contextSource()
         .root("dc=guo,dc=com");       //.url()
         .ldif("classpath:users.ldif");     //這裏是能夠分開放的,須要定義users.ldif文件
}

七、攔截請求

在任何的應用中,並非全部的頁面都須要同等程度地保護。儘管用戶基本信息頁面時公開的。可是,若是當處理「/spitter/me」時,經過展示當前用戶的基本信息那麼就須要進行認證,從而肯定要展示誰的信息。

對每一個請求進行細粒度安全性控制的關鍵在於重載configure(HttpSecurity)方法。

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("spitters/me").authenticated()                  //進行認證。
            .antMatchers(HttpMethod.POST,"/spittles").authenticated()            //必須通過認證
            .anyRequest().permitAll();                                                                    //其餘全部請求都是容許的,不須要認證。
}

antMatchers()方法中設置的路徑支持Ant風格的通配符。

.antMatchers("spitters/**").authenticated()
.antMatchers("spitters/**","spittles/mine").authenticated()
.antMatchers("spitters/.*").authenticated()
@Override
protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                        .antMatchers("spitters/me").authenticated()
                        .antMatchers(HttpMethod.POST, "/spittles")
                        .hasAuthority("ROLE_SPITTER")
                        .anyRequest().permitAll();
}

要求用戶不只須要認證,還要具有ROLE_SPITTER權限。做爲替代方案,還可使用hasRole()方法,它會自動使用「ROLE_」前綴

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                        .antMatchers("spitters/me").authenticated()
                        .antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
                        .anyRequest().permitAll();
}

很重要的一點是將最爲具體的請求路徑放到最前面,而最不具體的路徑放到最後面,若是不這樣作的話,那不具體的配置路徑將會覆蓋掉更爲具體的路徑配置

八、使用Spirng表達式進行安全保護

比SpEL更爲強大的緣由在於,HasRole()僅僅是Spring支持的安全相關表達式中的一種。

Spring Security支持的全部表達式。

  • principal : 用戶的principl對象
  • permitAll :結果始終爲true
  • hasRole 若是用戶被授予了指定的角色 結果爲true
  • authentication :用戶認證對象
  • denyAll 結果始終爲false
  • 。。。。

在掌握了Spring Security 的SpEL表達式後,咱們就可以再也不侷限於基於用戶的權限進訪問限制了。

九、強制通道的安全性

使用HTTP提交數據是一件具備風險的事情。經過HTTP發送的數據沒有通過加密,黑客就有機會攔截請求而且可以看到他們想看到的信息。這就是爲何銘感的 數據要經過HTTPS來加碼發送的緣由。

使用HTTPS彷佛很簡單,你要作的事情只是在URL中的HTTP後加上一個字母「s」就能夠了,是嗎? 是的,不加也能夠的。哈哈哈。。。

這是真的,但這是把使用的HTTPS通道的責任放在了錯誤的地方。

爲了保證註冊表單的數據經過HTTPS傳遞,咱們能夠在配置中添加requiresChannel()方法

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("spitters/me").authenticated()
            .antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
            .anyRequest().permitAll()
        .and()
            .requiresChannel()
            .antMatchers("/spitter/form").requiresSecure();   //須要HTTPS
}

不論什麼時候,只要是對「/spitter/form」的請求,Spring Security 都視爲須要安全通道(經過調用requiresChannel()肯定)並自動將請求重定向到HTTPS上。

與之相反,有些頁面並不須要設置經過HTTPS傳遞。將首頁聲明爲始終經過HTTP傳送。

.antMatchers("/").requiresInecure();

若是經過HTTPS發送了對"/"的請求,Spring Security將會把請求重定向到不安全的HTTP通道上。

十、防止跨站請求僞造

若是POST的請求來源於其餘站點的話,跨站請求僞造(cross-site request forgery CSRF),簡單來說,若是一個站點欺騙用戶提交請求到其餘服務器上的話,就會發生CSRF攻擊,這可能會帶來消極的後果。從Spring Security 3.2開始,默認就會啓用CSRF防禦。實際上,除非你 採起行爲處理CSRF防禦或者將這個功能禁用。不然的話,在應用提交表單的時候會遇到問題。

Spring Security 經過一個同步的token的方式來實現CSRF防禦的功能。它將會攔截狀態變化的請求並檢查CSRF token,若是請求中不包含 CSRF token的話,或者token不能與服務器端的token相匹配,請求將會失敗,並拋出CsrfException異常。

這意味着在你的應用中,全部的表單必須在一個"_csrf"域中提交token,並且這個token必需要與服務器端計算並存儲的token一致。這樣的話當表單提交的時候,才能匹配。

好消息是Spirng Security已經簡化了將token放到請求屬性中這一任務。若是你使用Thymeleaf做爲頁面模板的話,只要<form>標籤的action屬性添加了Thymeleaf命名空間前綴 。那麼就會自動生成一個「_csrf」隱藏域:

<form methos="POST" th:action="@{/spittles}"
..
</form>

若是使用JSP做爲模板的話

<input typt="hidden"
        name="${_csrf.parameterName}"
        value="${_csrf.token}"

處理CSRF的另外一種方式就是根本不去處理它,能夠在配置中經過調用csrf()和.disable()禁用Spring Security的CSRF防禦功能。

@Override
protected void configure(HttpSecurity http) throws Exception {
                .and()
                .csrf()
                .disable()
}

須要提醒的是:禁用CSRF防禦功能一般來說並非一個好主意。

十、認證用戶

formLogin方法啓用了基本的登陸頁功能

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()    啓用默認的登陸頁
        .and()
        .authorizeRequests()
            .antMatchers("/").authenticated()
            .antMatchers("/spitter/me").authenticated()
            .antMatchers(HttpMethod.POST, "/spittles").authenticated()
            .anyRequest().permitAll();
}

添加自定義的登陸頁面

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Spitter</title>
    <link rel="stylesheet"
          type="text/css"
          th:href="@{/resources/style.css}"></link>
  </head>
  <body onload='document.f.username.focus();'>
    <div id="header" th:include="page :: header"></div>

  <div id="content">

    <a th:href="@{/spitter/register}">Register</a>

  <form name='f' th:action='@{/login}' method='POST'>
   <table>
    <tr><td>User:</td><td>
        <input type='text' name='username' value='' /></td></tr>
    <tr><td>Password:</td>
        <td><input type='password' name='password'/></td></tr>
    <tr><td colspan='2'>
    <input id="remember_me" name="remember-me" type="checkbox"/>
    <label for="remember_me" class="inline">Remember me</label></td></tr>
    <tr><td colspan='2'>
        <input name="submit" type="submit" value="Login"/></td></tr>
   </table>
  </form>
  </div>
  <div id="footer" th:include="page :: copy"></div>
  </body>
</html>

須要注意的是,在Thymeleaf模板中,包含了username和Password輸入域,就像默認的登陸頁同樣,它也提交到了相對於上下文的「/login」頁面上,由於這是一個Thymeleaf模板,所以隱藏了"_csrf"域將自動添加到表單中。

十一、啓用HTTP Basic認證

對於應用程序的人類用戶來講,基於表單的認證是比較理想的,第十六章REST API 就不合適了。

HTTP Basic 認證會直接經過HTTP請求文自己,對要訪問的應用程序的用戶進行認證。當在Web瀏覽器中使用時,他將向用戶彈出一個簡單的模態對話框。

若是要啓用HTTP Basic認證的話,只需在configure()方法所傳入的HTTPSecurity對象上調用HTTPBasic()方法既可,另外還能夠調用realmName()方法指定域。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
        .and()
         .httpBasic()
             .realmName("Spittr")
        .and()

在configure中,經過調用add()方法來將不一樣的配置指令鏈接在一塊兒。

啓用remember-me

不須要每次都認證了。

.and()
.rememberMe()
    .tokenRepository(new InMemoryTokenRepositoryImpl())
    .tokenValiditySeconds(2419200)
    .key("spittrKey")
.and()

默認狀況下,這個功能經過在cookie中存儲一個token完成的,這個token最多兩週有效。可是,在這裏咱們指定這個token最多四周有效 (2,419,200秒)。

在登陸表單中,增長一個簡單複選框就能夠完成這件事
<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label>

在應用中,與登陸通用重要的就是退出,

退出功能

退出功能是經過Servlet的Filter實現的。這個Filter會攔截針對「/logout」的請求,所以,爲應用添加退出功能只須要添加以下的連接便可。

<a th:href="@{/logout}">Logout</a>

當用戶點擊這個連接的時候 ,會發起對「/logout」的請求,這個請求會被Spring Security的LogouFilter所處理,用戶會退出應用,全部的Remember-me token都會被清除。在退出完成後,用戶瀏覽器將會重定向到「/login?logout」,從而容許用戶在此登陸

若是你但願用戶被重定向到其餘的頁面,如應用的首頁,那麼能夠在configure()中進行以下的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
       http
           .formLogin()
               .loginPage("/login")
       .and()
           .logout()
               .logoutSuccessUrl("/")

到目前爲止,咱們已經看到了如何在發起請求的時候保護Web應用。接下來,咱們將會看一下如何添加視圖級別的安全性。

十一、保護視圖

Spring Security 的JSP標籤庫

  • <security:accesscontrollist> 若是用戶經過訪問控制列表授予了指定的權限,那麼渲染該標籤的內容
  • <security:authentication> 渲染當前用戶認證的詳細信息
  • <security:authorize> 若是用戶被授予了特定的權限那麼渲染該標籤的內容

爲了使用標籤庫首先須要聲明

<%@ taglib  prefix="security"
        url="http://www.springframework.org.security/tags"

待續,,好累,你們鼓勵下我好嗎?點贊,評論均可以。

12 小節(hahaha)

對於許多應用而言,安全性都是很是重要的切面。Spirng Security 提供了一種簡單、靈活且強大的機制來保護咱們的應用程序。

藉助於一系列Servlet Filte,Spring Security 可以控制對Web資源的訪問,包括Spring MVC控制器,藉助於Spring Security的Java配置模型,咱們沒必要直接處理Filter,可以很是簡潔地聲明爲Web安全性功能。

當認證用戶時,Spring Security提供了多種選項,咱們探討了如何基於內存用戶庫,關係型數據庫和LDAP目錄服務器來配置認證功能。若是這些可選方案沒法知足你的需求的話,咱們還學習力如何建立和配置自定義的用戶服務。

在前面的幾章中,咱們看到了如何將Spring運用到應用程序的前端,在接下來的章中,咱們還會繼續深刻這個技術棧,學習Spring如何在後端發揮做用,下一章將會首先從Spring的JDBC抽象開始。

期待》》》》。。。。

相關文章
相關標籤/搜索