您好,登錄后才能下訂單哦!
這篇文章主要介紹Spring gateway和Oauth2如何實現單點登錄,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
按職能,鑒權系統需要劃分 網關(spring gateway) + 鑒權(auth-server)。本文通過實踐搭建鑒權系統。
首先引入pom依賴
1、resilience 熔斷器
2、gateway 網關
3、eureka client 服務注冊中心
4、lombok插件
5、actuator狀態監控
<dependencies> <!-- 熔斷器--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-feign</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Gateway --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- 注冊中心 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
1、gateway信息
2、actuator狀態信息
3、配置eureka server地址及注冊信息
4、日志配置
5、獲取oauth3的jwt key
server: port: 18890 spring: application: name: open-api-gateway profiles: active: local cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true globalcors: corsConfigurations: '[/**]': allowedOrigins: "*" allowedMethods: "*" default-filters: - AddRequestParameter=gateway_type, member - AddRequestHeader=gateway_type, member management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always eureka: instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://127.0.0.1:22001/eureka logging: level: com.dq.edu: debug com: netflix: discovery: error spring: security: oauth3: resourceserver: jwt: jwk-set-uri: http://127.0.0.1:18889/auth-server/private/jwk_public_key
gateway 項目目錄
核心內容:security配置、PermissionFilter鑒權過濾器
1、security配置
package com.digquant.openapigateway.config; import com.digquant.openapigateway.entity.Response; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth3.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth3.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.Charset; @Slf4j @Configuration @AllArgsConstructor @EnableWebFluxSecurity public class SecurityConfig { private final ReactiveJwtDecoder jwtDecoder; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.cors().disable().csrf().disable(); http .authorizeExchange() .pathMatchers("/**").permitAll() .pathMatchers("/**/public/**").permitAll() .pathMatchers("/**/static/**").permitAll() .pathMatchers("/*/oauth/**").permitAll() .pathMatchers("/actuator/**").permitAll() .pathMatchers(HttpMethod.OPTIONS).permitAll() .anyExchange().authenticated() .and() .exceptionHandling() .accessDeniedHandler(serverAccessDeniedHandler()) .authenticationEntryPoint(serverAuthenticationEntryPoint()) .and() .oauth3ResourceServer() .jwt() .jwtDecoder(jwtDecoder) .and() .bearerTokenConverter(new ServerBearerTokenAuthenticationConverter()); return http.build(); } @Bean public ServerAccessDeniedHandler serverAccessDeniedHandler() { return (exchange, denied) -> { log.debug("沒有權限"); String errMsg = StringUtils.hasText(denied.getMessage()) ? denied.getMessage() : "沒有權限"; Response result = new Response(1, errMsg); return create(exchange, result); }; } @Bean public ServerAuthenticationEntryPoint serverAuthenticationEntryPoint() { return (exchange, e) -> { log.debug("認證失敗"); String errMsg = StringUtils.hasText(e.getMessage()) ? e.getMessage() : "認證失敗"; Response result = new Response(1, errMsg); return create(exchange, result); }; } private Mono<Void> create(ServerWebExchange exchange, Response result) { return Mono.defer(() -> Mono.just(exchange.getResponse())) .flatMap(response -> { response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); DataBufferFactory dataBufferFactory = response.bufferFactory(); DataBuffer buffer = dataBufferFactory.wrap(createErrorMsg(result)); return response.writeWith(Mono.just(buffer)) .doOnError(error -> DataBufferUtils.release(buffer)); }); } private byte[] createErrorMsg(Response result) { return result.getErrMsg().getBytes(Charset.defaultCharset()); } }
總結:
gateway是基于 WebFlux的響應式編程框架,所以在使用securityConfig時采用的注解是@EnableWebFluxSecurity
2、PermissionFilter
package com.digquant.openapigateway.filter; import com.digquant.openapigateway.utils.IStrings; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth3.jwt.Jwt; import org.springframework.security.oauth3.server.resource.authentication.AbstractOAuth3TokenAuthenticationToken; import org.springframework.security.oauth3.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import java.util.Objects; @Slf4j @Order @Component @AllArgsConstructor public class PermissionFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return ReactiveSecurityContextHolder.getContext() //排除沒有Token的 .filter(Objects::nonNull) // //檢查該路徑是否需要權限 // .filter(var -> permissionStore.usingPermission(exchange.getRequest().getPath().value())) .map(SecurityContext::getAuthentication) .map(authentication -> (JwtAuthenticationToken) authentication) .doOnNext(jwtAuthenticationToken -> { String path = exchange.getRequest().getPath().value(); log.info("請求 uri {}", path); }) .map(AbstractOAuth3TokenAuthenticationToken::getPrincipal) .map(var -> (Jwt) var) .map(jwt -> { String tokenValue = jwt.getTokenValue(); ServerHttpRequest.Builder builder = exchange.getRequest().mutate(); builder.header(HttpHeaders.AUTHORIZATION, IStrings.splice("Bearer ", tokenValue)); ServerHttpRequest request = builder.build(); return exchange.mutate().request(request).build(); }) .defaultIfEmpty(exchange) .flatMap(chain::filter); } }
總結
1、使用permissionStore來記錄uri的權限要求
2、獲取到jwtToken時,處理token所攜帶的權限,用于匹配是否能請求對應資源
OAuth3 項目目錄
1、eureka client
2、spring boot mvc
3、redis 用于存儲jwt
4、mysql用于記錄用戶資源權限
5、oauth3組件
6、httpclient fregn用于用戶登陸鑒權
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency> <!-- oauth3--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth3</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth3-jose</artifactId> <version>5.2.2.RELEASE</version> </dependency> <!-- HttpClient --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.2.RELEASE</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>10.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
應用配置
server: port: 37766 spring: application: name: auth-server mvc: throw-exception-if-no-handler-found: true profiles: active: dev mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.digquant.enity.po logging: level: com: digquant: dao: info file: path: /dq/log/new/auth-server digquant: authorization: auth-jwt-jks: hq-jwt.jks auth-jwt-key: hq-jwt auth-jwt-password: hq940313 access-token-validity-seconds: 14400 refresh-token-validity-seconds: 86400
1、AuthorizationServerConfig配置
package com.digquant.config; import com.digquant.dao.CustomRedisTokenStore; import com.digquant.enity.JWTProperties; import com.digquant.enity.Response; import com.digquant.service.OAuthUserService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth3.common.exceptions.OAuth3Exception; import org.springframework.security.oauth3.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth3.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth3.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth3.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth3.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth3.provider.ClientDetailsService; import org.springframework.security.oauth3.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth3.provider.error.WebResponseExceptionTranslator; import org.springframework.security.oauth3.provider.token.DefaultTokenServices; import org.springframework.security.oauth3.provider.token.TokenStore; import org.springframework.security.oauth3.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth3.provider.token.store.KeyStoreKeyFactory; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; import java.io.IOException; @Slf4j @Configuration @AllArgsConstructor @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private final JWTProperties jwtProperties; /** * 注入權限驗證控制器 支持 password grant type */ private final AuthenticationManager authenticationManager; /** * 數據源 */ private final DataSource dataSource; /** * 開啟refresh_token */ private final OAuthUserService userService; /** * 采用redis 存儲token */ private final RedisConnectionFactory redisConnectionFactory; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients() .checkTokenAccess("permitAll()") .tokenKeyAccess("permitAll()") .authenticationEntryPoint(authenticationEntryPoint()) .accessDeniedHandler(accessDeniedHandler()); } @Bean public AccessDeniedHandler accessDeniedHandler() { return (request, response, accessDeniedException) -> { Response result = new Response(1, accessDeniedException.getMessage()); writerResponse(response, result, HttpStatus.FORBIDDEN.value()); }; } @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return (request, response, authException) -> { Response result = new Response(1, authException.getMessage()); writerResponse(response, result, HttpStatus.UNAUTHORIZED.value()); }; } private void writerResponse(HttpServletResponse response, Response result, int status) throws IOException { response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setCharacterEncoding("UTF-8"); response.getWriter().print(result.getErrMsg()); response.getWriter().flush(); } @Bean("redisTokenStore") public TokenStore redisTokenStore() { return new CustomRedisTokenStore(redisConnectionFactory); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory().getKeyPair(jwtProperties.getAuthJwtKey())); return jwtAccessTokenConverter; } @Bean public KeyStoreKeyFactory keyStoreKeyFactory() { return new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getAuthJwtJks()), jwtProperties.getAuthJwtPassword().toCharArray()); } @Bean public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(redisTokenStore()); defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter()); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setReuseRefreshToken(false); defaultTokenServices.setAccessTokenValiditySeconds(jwtProperties.getAccessTokenValiditySeconds()); defaultTokenServices.setRefreshTokenValiditySeconds(jwtProperties.getRefreshTokenValiditySeconds()); return defaultTokenServices; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //開啟密碼授權類型 endpoints .authenticationManager(authenticationManager) //配置token存儲方式 .tokenStore(redisTokenStore()) //需要額外配置,用于refres_token .userDetailsService(userService) // .tokenServices(tokenServices()) .accessTokenConverter(jwtAccessTokenConverter()) .exceptionTranslator(exceptionTranslator()); } @Bean public WebResponseExceptionTranslator exceptionTranslator() { return exception -> { return ResponseEntity.status(HttpStatus.OK).body(new OAuth3Exception(exception.getMessage())); }; } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.withClientDetails(clientDetails()); clients.inMemory() .withClient("open_api") .authorizedGrantTypes("password","refresh_token") .authorities("USER") .scopes("read", "write") .resourceIds("auth-server") .secret(new BCryptPasswordEncoder().encode("digquant")); } }
2、ResourceServerConfig 資源服務配置
package com.digquant.config; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth3.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth3.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth3.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; @Order(6) @Configuration @AllArgsConstructor @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private final AccessDeniedHandler accessDeniedHandler; private final AuthenticationEntryPoint authenticationEntryPoint; @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) .and().authorizeRequests() .antMatchers("/swagger-ui.html","/webjars/**").permitAll() .antMatchers("/oauth/**").permitAll() .antMatchers("/actuator/**").permitAll() .antMatchers("/").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().permitAll(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) .resourceId("auth-server"); } }
3、SecurityConfig配置
package com.digquant.config; import com.digquant.service.CustomAuthenticationProvider; import com.digquant.service.OAuthUserService; import lombok.AllArgsConstructor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; 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.builders.WebSecurity; 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; @Order(7) @Configuration @EnableWebSecurity @AllArgsConstructor @AutoConfigureAfter(ResourceServerConfig.class) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final OAuthUserService userService; @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(); http.authorizeRequests() .antMatchers("/oauth/**").permitAll() .antMatchers("/public/**").permitAll() .antMatchers("/actuator/**").permitAll() .antMatchers("/private/**").permitAll() .antMatchers("/").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().permitAll() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/favor.ico"); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationProvider authenticationProvider() { CustomAuthenticationProvider provider = new CustomAuthenticationProvider() .setUserDetailsService(userService) .setPasswordEncoder(passwordEncoder()); provider.setHideUserNotFoundExceptions(false); return provider; } }
4、JwkController 用于gateway 請求jwt私鑰
package com.digquant.controller; import com.digquant.enity.JWTProperties; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.AllArgsConstructor; import org.springframework.security.oauth3.provider.token.store.KeyStoreKeyFactory; import org.springframework.web.bind.annotation.*; import java.security.interfaces.RSAPublicKey; import java.util.Map; @Api(tags = "jwk") @RestController @RequestMapping("/private") @AllArgsConstructor public class JwkController { private final KeyStoreKeyFactory keyStoreKeyFactory; private final JWTProperties jwtProperties; @ApiOperation("獲取jwk") @PostMapping("/jwk_public_key") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyStoreKeyFactory.getKeyPair(jwtProperties.getAuthJwtKey()).getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
注意私鑰放到項目的resources目錄下
5、用戶鑒權服務,獲取用戶信息
package com.digquant.service; import com.digquant.enity.to.AuthenticationTO; import com.digquant.enums.LoginType; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; @Slf4j @Component public class OAuthUserService implements UserDetailsService { @Autowired(required = false) private List<OAuthUserProcessor> oAuthUserProcessors; public UserDetails loadUser(String username, UsernamePasswordAuthenticationToken authentication) { AuthenticationTO authenticationTO = new AuthenticationTO(); authenticationTO.setUsername(username); authenticationTO.setPassword((String) authentication.getCredentials()); Map map = (Map) authentication.getDetails(); String scope = (String) map.get("scope"); String grantType = (String) map.get("grant_type"); String clientId = (String) map.get("client_id"); authenticationTO.setScope(scope); authenticationTO.setGrantType(grantType); authenticationTO.setLoginType(LoginType.PASSWORD); authenticationTO.setClientId(clientId); if (log.isDebugEnabled()) { log.debug("請求認證參數:{}", authenticationTO); } if (!CollectionUtils.isEmpty(oAuthUserProcessors)) { //目前只支持客戶端密碼登錄方式 for (OAuthUserProcessor oAuthUserProcessor : oAuthUserProcessors) { if (oAuthUserProcessor.support(authenticationTO)) { UserDetails userDetails = oAuthUserProcessor.findUser(authenticationTO); //TODO 需要加載OpenApi用戶的權限 loadAuthorities(userDetails, authenticationTO); return userDetails; } } } throw new UsernameNotFoundException("用戶不存在"); } private void loadAuthorities(UserDetails userDetails, AuthenticationTO authenticationTO) { } @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return null; } }
獲取token
refresh_token
以上是“Spring gateway和Oauth2如何實現單點登錄”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。