Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Spring Boot integrates Spring Security to realize OAuth 2.0 login

2025-04-01 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Internet Technology >

Share

Shulou(Shulou.com)06/03 Report--

The Spring Security OAuth project has been deprecated and the latest OAuth 2.0 support is provided by Spring Security. At present, Spring Security does not support Authorization Server and still needs to use the Spring Security OAuth project, but it will eventually be completely replaced by Spring Security.

This paper introduces the basic knowledge of Spring Security OAuth3 Client and how to use Spring Security to implement Wechat OAuth 2.0 login. GitHub source code wechat-api.

Spring Boot version: 2.2.2.RELEASE

To use Spring Security OAuth3 Client, simply add the following dependencies to the Spring Boot project:

Dependencies {implementation 'org.springframework.boot:spring-boot-starter-oauth3-client' implementation' org.springframework.boot:spring-boot-starter-security'... TestImplementation ('org.springframework.boot:spring-boot-starter-test') {exclude group:' org.junit.vintage', module: 'junit-vintage-engine'} testImplementation' org.springframework.security:spring-security-test'} GitHub login

Spring Security (CommonOAuth3Provider) predefines the OAuth Client configuration for Google, GitHub, Facebook, and Okta, where GitHub is defined as follows:

Private static final String DEFAULT_REDIRECT_URL = "{baseUrl} / {action} / oauth3/code/ {registrationId}"; GITHUB {@ Override public Builder getBuilder (String registrationId) {ClientRegistration.Builder builder = getBuilder (registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope ("read:user"); builder.authorizationUri ("https://github.com/login/oauth/authorize");") Builder.tokenUri ("https://github.com/login/oauth/access_token"); builder.userInfoUri (" https://api.github.com/user"); builder.userNameAttributeName ("id"); builder.clientName ("GitHub"); return builder;}}

To achieve GitHub OAuth login, you only need two steps:

Configure OAuth App

Log in to GitHub, enter Settings-> Developer settings-> OAuth Apps, and then click New OAuth App:

Where Authorization callback URL is OAuth Redirect URL, default is {baseUrl} / login/oauth3/code/ {registrationId}, and registrationId is github. Here we can enter http://localhost/login/oauth3/code/github only for testing.

Client ID and Client Secret are generated after saving.

Configure GitHub Clientspring: security: oauth3: client: registration: github: client-id: 34fbdcaae11111111111 client-secret: ca32a5ea5ad4b357777777777777777777777777

Launch the Spring Boot project after configuration, and access from the browser will automatically jump to the GitHub login page:

If more than one Client is configured, it jumps to the login selection page:

By default, OAuth 2.0 LoginPage is automatically generated by DefaultLoginPageGeneratingFilter, with one link per clientName. The default link address is OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/ {registrationId}".

Wechat registered account

Register an account on Wechat open platform or Wechat public platform as needed. After successful registration, you will get Client ID and Client Secret. I won't repeat it any more.

I use the web licensing service of Wechat public platform. Wechat web page authorization is achieved through OAuth3.0 's Authorization Code mechanism:

The user goes to the authorization page to agree to authorization. Get code in exchange for web authorization access_token through code (different from access_token in basic support) obtain basic user information through web authorization access_token and openid (support UnionID mechanism) configure Wechat Clientspring: security: oauth3: client: registration: weixin: client-id: wx2226666666666666 client-secret: 39899999999999999999999999999 redirect-uri: http:// Wechat.itrunner.org/login/oauth3/code/weixin authorization-grant-type: authorization_code scope: snsapi_userinfo client-name: WeiXin provider: weixin: authorization-uri: https://open.weixin.qq.com/connect/oauth3/authorize token-uri: https://api.weixin.qq.com/sns/oauth3/access_token user -info-uri: https://api.weixin.qq.com/sns/userinfo user-name-attribute: openid

It is explained that for the sake of safety, https should be used in practical application.

Custom implementation

The request parameters, request method and return type of Wechat OAuth 2.0 are inconsistent with the default implementation of Spring Security, and a custom implementation is required.

OAuth3LoginSecurityConfig

Package org.itrunner.wechat.config;import org.springframework.beans.factory.annotation.Value;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.oauth3.client.registration.ClientRegistrationRepository @ EnableWebSecuritypublic class OAuth3LoginSecurityConfig extends WebSecurityConfigurerAdapter {@ Value ("${security.ignore-paths}") private String [] ignorePaths; private final ClientRegistrationRepository clientRegistrationRepository; public OAuth3LoginSecurityConfig (ClientRegistrationRepository clientRegistrationRepository) {this.clientRegistrationRepository = clientRegistrationRepository;} @ Override public void configure (WebSecurity web) {web.ignoring () .antMatchers (ignorePaths) } @ Override protected void configure (HttpSecurity http) throws Exception {http.csrf () .disable () .headers () .oauth3Login (oauth3Login-> oauth3Login.authorizationEndpoint (authorizationEndpoint-> authorizationEndpoint.authorizationRequestResolver (new WeChatOAuth3AuthorizationRequestResolver (this.clientRegistrationRepository) .tokenEndpoint (tokenEndpoint-> TokenEndpoint.accessTokenResponseClient (new WeChatAuthorizationCodeTokenResponseClient ()) .userInfoEndpoint (userInfoEndpoint-> userInfoEndpoint.userService (new WeChatOAuth3UserService () .authorizeRequests (authorizeRequests-> authorizeRequests.anyRequest (). Authenticated ()) }}

Call oauth3Login () in configure (HttpSecurity http) to define the implementation of authorization, token, and userInfo.

Authorization

The link for Wechat to obtain code is as follows:

Https://open.weixin.qq.com/connect/oauth3/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

The default implementation of Spring Security is DefaultOAuth3AuthorizationRequestResolver, and the custom implementation WeChatOAuth3AuthorizationRequestResolver is as follows:

Package org.itrunner.wechat.config;import org.springframework.security.oauth3.client.registration.ClientRegistrationRepository;import org.springframework.security.oauth3.client.web.DefaultOAuth3AuthorizationRequestResolver;import org.springframework.security.oauth3.client.web.OAuth3AuthorizationRequestResolver;import org.springframework.security.oauth3.core.endpoint.OAuth3AuthorizationRequest;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import javax.servlet.http.HttpServletRequest;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_AUTHORIZATION_REQUEST_URL_FORMAT Import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID;import static org.springframework.security.oauth3.client.web.OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;public class WeChatOAuth3AuthorizationRequestResolver implements OAuth3AuthorizationRequestResolver {private static final String WEIXIN_DEFAULT_SCOPE = "snsapi_userinfo"; private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; private final OAuth3AuthorizationRequestResolver defaultAuthorizationRequestResolver; private final AntPathRequestMatcher authorizationRequestMatcher Public WeChatOAuth3AuthorizationRequestResolver (ClientRegistrationRepository clientRegistrationRepository) {this.defaultAuthorizationRequestResolver = new DefaultOAuth3AuthorizationRequestResolver (clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); this.authorizationRequestMatcher = new AntPathRequestMatcher (DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/ {" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");} @ Override public OAuth3AuthorizationRequest resolve (HttpServletRequest request) {String clientRegistrationId = this.resolveRegistrationId (request); OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve (request) Return resolve (authorizationRequest, clientRegistrationId);} @ Override public OAuth3AuthorizationRequest resolve (HttpServletRequest request, String clientRegistrationId) {OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve (request, clientRegistrationId); return resolve (authorizationRequest, clientRegistrationId);} private OAuth3AuthorizationRequest resolve (OAuth3AuthorizationRequest authorizationRequest, String registrationId) {if (authorizationRequest = = null) {return null } / / if it is not WeiXin, use the default implementation if (! WEIXIN_REGISTRATION_ID.equals (registrationId)) {return authorizationRequest } / / Wechat AuthorizationRequest URL String authorizationRequestUri = String.format (WEIXIN_AUTHORIZATION_REQUEST_URL_FORMAT, authorizationRequest.getAuthorizationUri (), authorizationRequest.getClientId (), encodeURL (authorizationRequest.getRedirectUri ()), authorizationRequest.getResponseType () .getValue (), getScope (authorizationRequest), authorizationRequest.getState ()); OAuth3AuthorizationRequest.Builder builder = OAuth3AuthorizationRequest.from (authorizationRequest); builder.authorizationRequestUri (authorizationRequestUri); return builder.build () } private String resolveRegistrationId (HttpServletRequest request) {if (this.authorizationRequestMatcher.matches (request)) {return this.authorizationRequestMatcher.matcher (request). GetVariables (). Get (REGISTRATION_ID_URI_VARIABLE_NAME);} return null;} private static String encodeURL (String url) {try {return URLEncoder.encode (url, "UTF-8") } catch (UnsupportedEncodingException e) {/ / The system should always have the platform default return null;}} private static String getScope (OAuth3AuthorizationRequest authorizationRequest) {return authorizationRequest.getScopes (). Stream (). FindFirst (). OrElse (WEIXIN_DEFAULT_SCOPE);}}

Access Token

The link for Wechat to obtain Access Token is as follows:

Https://api.weixin.qq.com/sns/oauth3/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

The default implementation classes of Spring Security are DefaultAuthorizationCodeTokenResponseClient, OAuth3AuthorizationCodeGrantRequestEntityConverter and OAuth3AccessTokenResponse, and the custom implementations are WeChatAuthorizationCodeTokenResponseClient, WeChatAuthorizationCodeGrantRequestEntityConverter and WeChatAccessTokenResponse, respectively.

WeChatAuthorizationCodeTokenResponseClient executes the request to get Token:

Package org.itrunner.wechat.config;import lombok.extern.slf4j.Slf4j;import org.springframework.core.convert.converter.Converter;import org.springframework.http.RequestEntity;import org.springframework.http.ResponseEntity;import org.springframework.security.oauth3.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;import org.springframework.security.oauth3.client.endpoint.OAuth3AccessTokenResponseClient;import org.springframework.security.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;import org.springframework.security.oauth3.client.http.OAuth3ErrorResponseErrorHandler;import org.springframework.security.oauth3.core.OAuth3AuthorizationException Import org.springframework.security.oauth3.core.OAuth3Error;import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;import org.springframework.util.Assert;import org.springframework.util.CollectionUtils;import org.springframework.web.client.RestClientException;import org.springframework.web.client.RestOperations;import org.springframework.web.client.RestTemplate;import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID;@Slf4jpublic class WeChatAuthorizationCodeTokenResponseClient implements OAuth3AccessTokenResponseClient {private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response" Private Converter request = this.requestEntityConverter.convert (authorizationCodeGrantRequest); ResponseEntity response; try {/ / execute request response = this.restOperations.exchange (request, String.class);} catch (RestClientException ex) {String description = "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response:"; log.error (description, ex) OAuth3Error oauth3Error = new OAuth3Error (INVALID_TOKEN_RESPONSE_ERROR_CODE, description + ex.getMessage (), null); throw new OAuth3AuthorizationException (oauth3Error, ex);} / / parsed response OAuth3AccessTokenResponse tokenResponse = WeChatAccessTokenResponse.build (response.getBody ()). ToOAuth3AccessTokenResponse () If (CollectionUtils.isEmpty (tokenResponse.getAccessToken (). GetScopes ()) {tokenResponse = OAuth3AccessTokenResponse.withResponse (tokenResponse) .scopes (authorizationCodeGrantRequest.getClientRegistration (). GetScopes ()) .build ();} return tokenResponse;}}

WeChatAuthorizationCodeGrantRequestEntityConverter builds Access Token RequestEntity:

Package org.itrunner.wechat.config;import lombok.extern.slf4j.Slf4j;import org.springframework.core.convert.converter.Converter;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.MediaType;import org.springframework.http.RequestEntity;import org.springframework.security.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;import org.springframework.security.oauth3.client.registration.ClientRegistration;import org.springframework.web.util.UriComponentsBuilder;import java.net.URI;import java.util.Collections Import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_ACCESS_TOKEN_URL_FORMAT;@Slf4jpublic class WeChatAuthorizationCodeGrantRequestEntityConverter implements Converter convert (OAuth3AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {HttpHeaders headers = getTokenRequestHeaders (); URI uri = buildUri (authorizationCodeGrantRequest); return new RequestEntity (headers, HttpMethod.GET, uri);} private HttpHeaders getTokenRequestHeaders () {HttpHeaders headers = new HttpHeaders (); headers.setAccept (Collections.singletonList (MediaType.TEXT_PLAIN)); return headers } private URI buildUri (OAuth3AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration (); String tokenUri = clientRegistration.getProviderDetails (). GetTokenUri (); String appid = clientRegistration.getClientId (); String secret = clientRegistration.getClientSecret (); String code = authorizationCodeGrantRequest.getAuthorizationExchange (). GetAuthorizationResponse (). GetCode (); String grantType = authorizationCodeGrantRequest.getGrantType (). GetValue () String uriString = String.format (WEIXIN_ACCESS_TOKEN_URL_FORMAT, tokenUri, appid, secret, code, grantType); return UriComponentsBuilder.fromUriString (uriString). Build (). ToUri ();}}

WeChatAccessTokenResponse parses Response:

Package org.itrunner.wechat.config;import com.fasterxml.jackson.annotation.JsonProperty;import com.fasterxml.jackson.core.JsonProcessingException;import lombok.Getter;import lombok.Setter;import lombok.extern.slf4j.Slf4j;import org.itrunner.wechat.util.JsonUtils;import org.springframework.security.oauth3.core.OAuth3AccessToken;import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;import java.util.*;@Getter@Setter@Slf4jpublic class WeChatAccessTokenResponse {@ JsonProperty ("access_token") private String accessToken @ JsonProperty ("expires_in") private Long expiresIn; @ JsonProperty ("refresh_token") private String refreshToken; private String openid; private String scope; private WeChatAccessTokenResponse () {} public static WeChatAccessTokenResponse build (String json) {try {return JsonUtils.parseJson (json, WeChatAccessTokenResponse.class) } catch (JsonProcessingException e) {log.error ("An error occurred while attempting to parse the WeiXin AccessTokenResponse:" + e.getMessage ()); return null;}} public OAuth3AccessTokenResponse toOAuth3AccessTokenResponse () {OAuth3AccessTokenResponse.Builder builder = OAuth3AccessTokenResponse.withToken (accessToken); builder.tokenType (OAuth3AccessToken.TokenType.BEARER); builder.expiresIn (expiresIn); builder.refreshToken (refreshToken) String [] scopes = scope.split (","); Set scopeSet = new HashSet (); Collections.addAll (scopeSet, scopes); builder.scopes (scopeSet); Map additionalParameters = new LinkedHashMap (); additionalParameters.put ("openid", openid); builder.additionalParameters (additionalParameters); return builder.build ();}}

User Info

The link for Wechat to obtain User Info is as follows:

Https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

The default implementation classes of Spring Security are DefaultOAuth3UserService, OAuth3UserRequestEntityConverter and DefaultOAuth3User, and the custom implementations are WeChatOAuth3UserService, WeChatUserRequestEntityConverter and WeChatOAuth3User, respectively.

WeChatOAuth3UserService executes the request to get User Info:

Package org.itrunner.wechat.config;import lombok.extern.slf4j.Slf4j;import org.springframework.core.convert.converter.Converter;import org.springframework.http.RequestEntity;import org.springframework.http.ResponseEntity;import org.springframework.security.oauth3.client.http.OAuth3ErrorResponseErrorHandler;import org.springframework.security.oauth3.client.userinfo.DefaultOAuth3UserService;import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;import org.springframework.security.oauth3.client.userinfo.OAuth3UserService;import org.springframework.security.oauth3.core.OAuth3AuthenticationException Import org.springframework.security.oauth3.core.OAuth3AuthorizationException;import org.springframework.security.oauth3.core.OAuth3Error;import org.springframework.security.oauth3.core.user.OAuth3User;import org.springframework.util.Assert;import org.springframework.util.StringUtils;import org.springframework.web.client.RestClientException;import org.springframework.web.client.RestOperations;import org.springframework.web.client.RestTemplate;import java.io.UnsupportedEncodingException;import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID @ Slf4jpublic class WeChatOAuth3UserService implements OAuth3UserService {private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri"; private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute"; private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; private Converter request = this.requestEntityConverter.convert (userRequest); ResponseEntity response Try {/ / execute request response = this.restOperations.exchange (request, String.class);} catch (OAuth3AuthorizationException ex) {OAuth3Error oauth3Error = ex.getError (); StringBuilder errorDetails = new StringBuilder (); errorDetails.append ("Error details: [") ErrorDetails.append ("UserInfo Uri:") .append (userRequest.getClientRegistration (). GetProviderDetails (). GetUserInfoEndpoint (). GetUri ()); errorDetails.append (", ErrorCode:") .append (oauth3Error.getErrorCode ()); if (oauth3Error.getDescription ()! = null) {errorDetails.append (", Error Description:") .append (oauth3Error.getDescription ()) } errorDetails.append ("]"); oauth3Error = new OAuth3Error (INVALID_USER_INFO_RESPONSE_ERROR_CODE, "An error occurred while attempting to retrieve the UserInfo Resource:" + errorDetails.toString (), null); throw new OAuth3AuthenticationException (oauth3Error, oauth3Error.toString (), ex) } catch (RestClientException ex) {OAuth3Error oauth3Error = new OAuth3Error (INVALID_USER_INFO_RESPONSE_ERROR_CODE, "An error occurred while attempting to retrieve the UserInfo Resource:" + ex.getMessage (), null); throw new OAuth3AuthenticationException (oauth3Error, oauth3Error.toString (), ex);} / / parse response String userAttributes = response.getBody () Try {/ / Transcoding userAttributes = new String (userAttributes.getBytes ("ISO-8859-1"), "UTF-8");} catch (UnsupportedEncodingException e) {log.error ("An error occurred while attempting to encode userAttributes:" + e.getMessage ());} return WeChatOAuth3User.build (userAttributes, userNameAttributeName);}}

WeChatUserRequestEntityConverter builds Use Info RequestEntity:

Package org.itrunner.wechat.config;import org.springframework.core.convert.converter.Converter;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.MediaType;import org.springframework.http.RequestEntity;import org.springframework.security.oauth3.client.registration.ClientRegistration;import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;import org.springframework.web.util.UriComponentsBuilder;import java.net.URI;import java.util.Collections Import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_USER_INFO_URL_FORMAT;public class WeChatUserRequestEntityConverter implements Converter convert (OAuth3UserRequest userRequest) {HttpHeaders headers = getUserRequestHeaders (); URI uri = buildUri (userRequest); return new RequestEntity (headers, HttpMethod.GET, uri);} private HttpHeaders getUserRequestHeaders () {HttpHeaders headers = new HttpHeaders (); headers.setAccept (Collections.singletonList (MediaType.TEXT_PLAIN)); return headers } private URI buildUri (OAuth3UserRequest userRequest) {ClientRegistration clientRegistration = userRequest.getClientRegistration (); String uri = clientRegistration.getProviderDetails (). GetUserInfoEndpoint (). GetUri (); String accessToken = userRequest.getAccessToken (). GetTokenValue (); String openId = (String) userRequest.getAdditionalParameters (). Get ("openid"); String userInfoUrl = String.format (WEIXIN_USER_INFO_URL_FORMAT, uri, accessToken, openId, "zh_CN") Return UriComponentsBuilder.fromUriString (userInfoUrl). Build (). ToUri ();}

WeChatOAuth3User:

Package org.itrunner.wechat.config;import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.core.JsonProcessingException;import lombok.extern.slf4j.Slf4j;import org.itrunner.wechat.util.JsonUtils;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.oauth3.core.user.OAuth3User;import java.util.*;@Slf4jpublic class WeChatOAuth3User implements OAuth3User {private String openid; private String nickname; private int sex; private String language Private String city; private String province; private String country; private String headimgurl; private String [] privilege; @ JsonIgnore private Set authorities = new HashSet (); @ JsonIgnore private Map attributes; @ JsonIgnore private String nameAttributeKey; public static WeChatOAuth3User build (String json, String userNameAttributeName) {try {WeChatOAuth3User user = JsonUtils.parseJson (json, WeChatOAuth3User.class); user.nameAttributeKey = userNameAttributeName; user.setAttributes () User.setAuthorities (); return user;} catch (JsonProcessingException e) {log.error ("An error occurred while attempting to parse the weixin User Info Response:" + e.getMessage ()); return null;}} private void setAttributes () {attributes = new HashMap (); this.attributes.put ("openid", openid) This.attributes.put ("nickname", nickname); this.attributes.put ("sex", sex); this.attributes.put ("language", language); this.attributes.put ("city", city); this.attributes.put ("province", province); this.attributes.put ("country", country); this.attributes.put ("headimgurl", headimgurl) } private void setAuthorities () {authorities = new LinkedHashSet (); for (String authority: privilege) {authorities.add (new SimpleGrantedAuthority (authority));}} @ Override public Map getAttributes () {return this.attributes;} @ Override public Collection

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Internet Technology

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report