SpringSecurity實現OAuth2+JWT


一、基本概念

1.1 認證方式

1.1.1 基於session方式認證

他的流程是:用戶認證成功后,服務端生成相應的用戶數據保存在session中,發給客戶端的session_id保存在cookie中。這樣用戶請求時只要帶上session_id就可以驗證服務端是否存在session,以此完成用戶的校驗。當用戶退出系統或session過期時,客戶端的session_id也就無效了。

1.1.2 基於token認證方式

他的流程是:用戶認證成功后,服務端生成一個token發給客戶端,客戶端放到cookie或localStorage等存儲中,每次請求帶上token,服務端收到后就可以驗證。

1.2 什么是授權

授權:用戶認證通過后根據用戶的權限來控制用戶訪問資源的過程。

1.3 權限模型

最簡單權限表設計。

二、快速入門

2.1 用戶認證

先自行搭建一個SpringMvc或者SpringBoot項目.

2.1.1 引入依賴
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
2.1.2 配置類
@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {


    /**
     * 配置用戶信息服務
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }


    /**
     * 密碼編碼器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 安全攔截機制
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .successForwardUrl("/login-success"); 
    }

}

2.1.3 測試資源訪問

寫一個controller進行測試.


@RestController
public class ResourceController {


    @RequestMapping("/r/r1")
    public String r1(){
        return "訪問資源1";
    }

    @RequestMapping("/r/r2")
    public String r2(){
        return "訪問資源2";
    }
}

直接訪問http://localhost:8080/r/r2,會跳到登陸頁面,登陸成功后訪問則成功.

以上就利用SpringSecurity完成來了認證功能.

2.2 資源控制

只需在antMatchers("/r/r1").hasAnyAuthority("p1")方法上加上hasAnyAuthority就可以了.

這個方法代表要訪問/r/r1,必須得有p1權限.

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .successForwardUrl("/login-success");
    }

注意:規則的順序很重要,具體的規則要放在最上面,permitAll這種放在下面

三、工作原理

Spring Security對資源對保護是通過filter來實現對,當初始化Spring Security時,會創建一個名為SpringSecurityFilterChain的Servlet過濾器,類型為FilterChainProxy,他實現了javax.servlet.Filter接口,因此外部的請求會經過此類.

SpringSecurity的功能主要是通過過濾器鏈來完成的.

下面介紹幾個主要的攔截器:

  • SecurityContextPersistenceFilter:整個攔截過程的入口和出口
  • UsernamePasswordAuthenticationFilter:用於處理來自表單提交的認證
  • FilterSecurityInterceptor:用於保護web資源的
  • ExceptionTranslationFilter:能夠捕獲FilterChain的所有異常並處理.

認證過程:

3.1 改為從數據庫查詢用戶

實現UserDetailsService接口

@Service
public class MyUserDetailService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //這里可以寫從數據庫查的邏輯
        UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
        return userDetails;
    }
}

3.2 加密后的密碼校對

先將密碼加密器改為BCryptPasswordEncoder

@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

加密算法的使用

public static void main(String[] args) {
        //生成加鹽的密碼
        String hashpw = BCrypt.hashpw("123456", BCrypt.gensalt());
        //校驗密碼
        boolean checkpw = BCrypt.checkpw("123456", hashpw);
        System.out.print(checkpw);
    }

3.3 權限認證

授權流程:

AccessDecisionManager采用投票的方式來確定是否能夠訪問對應受保護的資源.

默認的實現是AffirmativeBased類

四、自定義頁面

4.1 自定義登陸頁面

package com.mmc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {




    /**
     * 密碼編碼器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全攔截機制
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //關閉csrf
        http.csrf().disable().
                authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                //登陸頁面
                .loginPage("/loginPage")
               //登陸請求的url .loginProcessingUrl("/userlogin")
                .successForwardUrl("/login-success");
    }

}

定義一個登陸頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸頁</title>
</head>
<body>

    <form action="/userlogin" method="post">
        <p>用戶名:<input name="username" type="text"> </p>
        <p>密碼:<input name="password" type="text"></p>
        <button type="submit">登陸</button>
    </form>


</body>
</html>

4.2 會話控制

4.2.1 獲取當前用戶信息
public String getUserInfo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用戶身份
        Object principal = authentication.getPrincipal();
        if(principal==null){
            return "";
        }
        if(principal instanceof UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            return userDetails.getUsername();
        }else {
            return principal.toString();
        }
    }
4.2.2 會話控制

我們可以通過下列選項控制會話何時創建及如何與SpringSecurity交互

機制 描述
always 沒有session存在就創建一個
ifRequired 如果有需要就創建一個登陸時(默認)
never SpringSecurity不會創建session,但是應用其他地方創建來的話,可以使用
stateless 不創建不使用

配置地方如下:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().
                authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/loginPage")
                .loginProcessingUrl("/userlogin")
                .successForwardUrl("/login-success")
                .and()
                //控制器
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

    }

4.3 自定義登出

可以配置如下選項:

 .and()
                .logout()
                .logoutSuccessUrl("/login-view")
                .addLogoutHandler(logoutHandle)
                .logoutSuccessHandler(logoutSuccessHandler);

4.4 授權

4.4.1 web方式授權
       http.csrf().disable().
                authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyAuthority("p2")
4.4.2 方法授權
  1. 配置類上加注解

@EnableGlobalMethodSecurity(securedEnabled = true)

  1. 方法上加注解
  @RequestMapping("/saveUser")
    @ResponseBody
    @PreAuthorize("hasAuthrity('p1')")
    public String saveUser(){
        User user=new User();
        user.setUsername("zhangsan");
        user.setPassword(BCrypt.hashpw("123456",BCrypt.gensalt()));
        user.setMobile("18380430770");
        userMapper.insert(user);
        return "sucess";
    }

五、分布式系統認證方案

5.1 分布式認證需求

統一的認證授權

提供獨立的認證服務,統一處理認證授權.無論上不同類型的用戶,還是不同類型的客戶端(web、app),均采用一致的認證、權限、會話機制,實現統一授權.

應用接入認證

應提供擴展和開放能力,提供安全的系統對接機制,並可開放部分API給 第三方使用.

5.2 分布式方案選型

5.2.1 采用session的方式

優點:安全、傳輸數據量小

缺點:分布式應用中需要同步session、session上基於coockie的,有的客戶端不支持coockie

session處理的三個方法:

  • session同步
  • session黏貼,即用戶去某服務器登陸,那么他的所有請求就都路由到指定服務器
  • session統一存儲.
5.2.2 采用token的方式

優點:第三方更適合接入,可使用當前流行的開放協議OAuth2.0和JWT

缺點:token中包含用戶信息,數據大,帶寬壓力大、token檢驗需要耗費CPU

六、OAuth2.0

6.1 概念介紹

OAuth是一個開放標准,允許用戶授權第三方應用訪問存儲在另外的服務器上的信息,而不用提供用戶名或密碼給第三方應用.

第三方登陸流程圖:

OAuth2.0角色介紹:

  1. 客戶端

包括安卓客戶端、瀏覽器、小程序等
2. 資源擁有者

通常是用戶,也可以是應用程序
3. 認證服務器

用於服務提供商對資源擁有的身份進行認證、對訪問資源進行授權.認證成功后發放令牌,作為訪問資源服務器的憑證.

  1. 資源服務器

存儲資源的服務器.

問題:

服務提供商會讓所有的客戶端接入到他的授權服務器嗎?答案是不能.他會給准入的接入方一個身份:

  • client_id:客戶端標識
  • client_secret:客戶端密鑰

6.2 環境搭建

6.2.1 創建項目

先自行創建一個springcloud微服務項目.父工程的pom文件為:

<?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">
    <modelVersion>4.0.0</modelVersion>
    <modules>
        <module>spring-security-uaa</module>
        <module>spring-security-order</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
    </parent>

    <groupId>com.mmc</groupId>
    <artifactId>spring-cloud-security-study</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>


    <dependencyManagement>

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>


            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>provided</scope>
            </dependency>

            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>


<!--            <dependency>-->
<!--                <groupId>mysql</groupId>-->
<!--                <artifactId>mysql-connector-java</artifactId>-->
<!--                <version>5.1.47</version>-->
<!--            </dependency>-->


            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>


            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>

        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

再在里面創建一個授權服務的module,pom文件:

<?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>spring-cloud-security-study</artifactId>
        <groupId>com.mmc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-security-uaa</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>



        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>

</project>
6.2.2 授權服務器配置
  1. 配置客戶端詳細信息

@Service
public class MyUserDetailService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails userDetails = User.withUsername(username).password("$2a$10$R5vdYffOXhN2ay0Cke9YIezhlEzHaMt4i8Ndl9GXTOQepSp8ixpVy").authorities("p1").build();
        return userDetails;
    }
}
@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("c1")
                .secret(new BCryptPasswordEncoder().encode("secret"))
                //資源列表
                .resourceIds("res1")
                //授權類型
                .authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
                //允許的授權范圍,all是自定義的字符串
                .scopes("all")
                //false代表跳轉到授權頁面
                .autoApprove(false)
                //驗證回調地址
                .redirectUris("http://www.baidu.com");
    }
  1. 管理令牌
@Configuration
public class TokenConfig {

    @Bean
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
}
@Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices services=new DefaultTokenServices();
        //客戶端信息
        services.setClientDetailsService(clientDetailsService);
        //是否產生刷新令牌
        services.setSupportRefreshToken(true);
        //令牌存儲策略
        services.setTokenStore(tokenStore);
        //令牌存活時間
        services.setAccessTokenValiditySeconds(60*5);
        services.setRefreshTokenValiditySeconds(60*10);
        return services;
    }
  1. 令牌訪問端點配置
package com.mmc.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {




    /**
     * 密碼編碼器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 認證管理器
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    /**
     * 安全攔截機制
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().
                authorizeRequests()
                .antMatchers("/r/r1")
                .hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();

    }

}

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;
    
    
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        //基於內存的授權碼模式
        return new InMemoryAuthorizationCodeServices();
    }
    
    
    /**
     * 令牌訪問端點配置
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.
                //密碼模式需要
                authorizationCodeServices(authorizationCodeServices)
                //授權碼模式需要
                .authenticationManager(authenticationManager)
                .tokenServices(tokenServices())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
  1. 令牌訪問端點安全配置
 /**
     * 令牌訪問端點安全配置
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }
  1. 框架默認的url鏈接
  • /oauth/authorize 授權端點
  • /oauth/token 獲取token
  • /oauth/conirm_access 用戶確認授權提交端點
  • /oauth/error 授權服務錯誤信息
  • /oauth/check_token 提供給資源服務使用的令牌解析端點
  • /oauth/token_key 提供公有密鑰的端點,如果你使用JWT令牌
6.2.3 授權模式
  1. 授權碼模式

步驟1:獲取code

請求示例:

http://localhost:8080/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_url=http://www.baidu.com

登陸之后回跳到授權頁面,點擊允許后,會跳轉到redirect_url,並顯示出code

步驟2:獲取token(注意如果請求方式配了POST就要用POST方式)

http://localhost:8080/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=meAXv5&redirect_url=http://www.baidu.com

即可獲取到token

授權碼模式是四種模式中最安全的模式.一般用於client是web服務端應用或第三方原生app調用資源服務的時候.

  1. 簡化模式

步驟1:直接拿token

http://localhost:8080/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_url=http://www.baidu.com

一般來說簡化模式用於沒有服務器應用的第三方單頁面應用,因為沒有服務器就沒法接收授權碼.

  1. 密碼模式

步驟1:

http://localhost:8080/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

這種模式非常簡單,但是卻會將用戶信息泄露給client,因此只能用於client是我們自己開發的情況.

  1. 客戶端模式

步驟1:

http://localhost:8080/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

6.3 JWT令牌

6.3.1 JWT簡介

Json web token(JWT)是一個開放的行業標准.定義了一種簡潔的,自包含的協議格式.用於在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任.JWT可以使用HMAC或RSA簽名,防止篡改.

JWT的優點:

  • 基於json,方便解析
  • 可以自定義內容,方便擴展
  • 通過非對稱加密算法及簽名,安全性高
  • 資源服務使用JWT可以不依賴認證服務即可完成授權.

JWT由以下三部分組成,每部分中間用.分割.如xxx.yyy.zzz

  1. header的部分

包括令牌的類型及使用的加密算法.

{
    "alg":"HS256",
    "typ":"JWT"
}

將上面的內容進行base64Url編碼,得到一個字符串就是JWT的第一部分.


  1. Payload

第二部分是負載,內容也是json對象,它是存放有效信息的地方,可以存JWT的現有字段,也可以自定義字段.此部分不建議放敏感信息,因為可以被解碼.最后將上面的內容進行base64Url編碼,得到一個字符串就是JWT的第二部分.
例子:

{
    "merchantid":123,
    "name":"wang"
}
  1. Signature

第三部分是簽名,防止內容被篡改.

例子:

HMACSH256(
base64UrlEncode(header)+.base64UrlEncode(payload),secret
)


secret:簽名使用的密鑰.

6.3.2 配置JWT
@Configuration
public class TokenConfig {

    public static final String SIGN_KEY = "abc123";

    @Bean
    public JwtAccessTokenConverter tokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter=new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGN_KEY);
        return jwtAccessTokenConverter;
    }

    @Bean
    public TokenStore tokenStore(){
       return new JwtTokenStore(tokenConverter());
    }
}

然后在配置生成令牌的地方,加一段增強令牌的代碼:

 /**
     * 令牌管理服務
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices services=new DefaultTokenServices();
        //客戶端信息
        services.setClientDetailsService(clientDetailsService);
        //是否產生刷新令牌
        services.setSupportRefreshToken(true);
        //令牌存儲策略
        services.setTokenStore(tokenStore);
        //令牌存活時間
        services.setAccessTokenValiditySeconds(60*5);
        services.setRefreshTokenValiditySeconds(60*10);

        //令牌增強
        TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);
        
        return services;
    }

完整的項目地址:https://gitee.com/mmcLine/spring-security-study


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM