温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在Spring Security中实现Oauth2授权

发布时间:2021-05-27 18:10:14 来源:亿速云 阅读:279 作者:Leah 栏目:编程语言

这篇文章给大家介绍怎么在Spring Security中实现Oauth2授权,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

Oauth3概述

oauth3根据使用场景不同,分成了4种模式

  • 授权码模式(authorization code)

  • 简化模式(implicit)

  • 密码模式(resource owner password credentials)

  • 客户端模式(client credentials)

在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。

Oauth3授权主要由两部分组成:

  • Authorization server:认证服务

  • Resource server:资源服务

在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。

准备阶段

核心maven依赖如下

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-web</artifactId>  </dependency>    <dependency>   <groupId>com.fasterxml.jackson.datatype</groupId>   <artifactId>jackson-datatype-joda</artifactId>  </dependency>  <dependency>   <groupId>org.thymeleaf.extras</groupId>   <artifactId>thymeleaf-extras-springsecurity4</artifactId>  </dependency>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-thymeleaf</artifactId>  </dependency>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-security</artifactId>  </dependency>  <dependency>   <groupId>org.springframework.security.oauth</groupId>   <artifactId>spring-security-oauth3</artifactId>  </dependency>    <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-jdbc</artifactId>  </dependency>  <dependency>   <groupId>mysql</groupId>   <artifactId>mysql-connector-java</artifactId>  </dependency>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-data-jpa</artifactId>  </dependency>

token的存储主流有三种方式,分别为内存、redis和数据库,在实际项目中通常使用redis和数据库存储。个人推荐使用mysql数据库存储。

初始化数据结构、索引和数据SQL语句如下:

-- -- Oauth sql -- MYSQL --   Drop table if exists oauth_client_details; create table oauth_client_details (  client_id VARCHAR(255) PRIMARY KEY,  resource_ids VARCHAR(255),  client_secret VARCHAR(255),  scope VARCHAR(255),  authorized_grant_types VARCHAR(255),  web_server_redirect_uri VARCHAR(255),  authorities VARCHAR(255),  access_token_validity INTEGER,  refresh_token_validity INTEGER,  additional_information TEXT,  autoapprove VARCHAR (255) default 'false' ) ENGINE=InnoDB DEFAULT CHARSET=utf8;     Drop table if exists oauth_access_token; create table oauth_access_token (  token_id VARCHAR(255),  token BLOB,  authentication_id VARCHAR(255),  user_name VARCHAR(255),  client_id VARCHAR(255),  authentication BLOB,  refresh_token VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;     Drop table if exists oauth_refresh_token; create table oauth_refresh_token (  token_id VARCHAR(255),  token BLOB,  authentication BLOB ) ENGINE=InnoDB DEFAULT CHARSET=utf8;     Drop table if exists oauth_code; create table oauth_code (  code VARCHAR(255),  authentication BLOB ) ENGINE=InnoDB DEFAULT CHARSET=utf8;       -- Add indexes create index token_id_index on oauth_access_token (token_id); create index authentication_id_index on oauth_access_token (authentication_id); create index user_name_index on oauth_access_token (user_name); create index client_id_index on oauth_access_token (client_id); create index refresh_token_index on oauth_access_token (refresh_token); create index token_id_index on oauth_refresh_token (token_id); create index code_index on oauth_code (code);   -- INSERT DEFAULT DATA INSERT INTO `oauth_client_details` VALUES ('dev', '', 'dev', 'app', 'authorization_code', 'http://localhost:7777/', '', '3600', '3600', '{"country":"CN","country_code":"086"}', 'TAIJI');

核心配置

核心配置主要分为授权应用和客户端应用两部分,如下:

  • 授权应用:即Oauth3授权服务,主要包括Spring Security、认证服务和资源服务两部分配置

  • 客户端应用:即通过授权应用进行认证的应用,多个客户端应用间支持单点登录

授权应用主要配置如下:

application.properties链接已初始化Oauth3的数据库即可

Application启动类,授权服务开启配置和Spring Security配置,如下:

@SpringBootApplication @AutoConfigureAfter(JacksonAutoConfiguration.class) @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) @EnableAuthorizationServer public class Application extends WebSecurityConfigurerAdapter {   public static void main(String[] args) {     SpringApplication.run(Application.class, args);   }     // 启动的时候要注意,由于我们在controller中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例   @Autowired   private RestTemplateBuilder builder;     // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例   @Bean   public RestTemplate restTemplate() {     return builder.build();   }     @Configuration   public class WebMvcConfig extends WebMvcConfigurerAdapter {     @Override     public void addViewControllers(ViewControllerRegistry registry) {       registry.addViewController("/login").setViewName("login");     }   }       @Override   protected void configure(HttpSecurity http) throws Exception {       http.headers().frameOptions().disable();     http.authorizeRequests()         .antMatchers("/403").permitAll() // for test         .antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access", "/appManager").permitAll() // for login         .antMatchers("/image", "/js/**", "/fonts/**").permitAll() // for login         .antMatchers("/j_spring_security_check").permitAll()         .antMatchers("/oauth/authorize").authenticated();     /*.anyRequest().fullyAuthenticated();*/     http.formLogin().loginPage("/login").failureUrl("/login?error").permitAll()         .and()         .authorizeRequests().anyRequest().authenticated()         .and().logout().invalidateHttpSession(true)         .and().sessionManagement().maximumSessions(1).expiredUrl("/login?expired").sessionRegistry(sessionRegistry());     http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());     http.rememberMe().disable();     http.httpBasic();     }   }

资源服务开启,如下:

@Configuration @EnableResourceServer protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {     @Override     public void configure(HttpSecurity http) throws Exception {       http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();     }   }

OAuth3认证授权服务配置,如下:

@Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {  public static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);    @Autowired   private AuthenticationManager authenticationManager;     @Autowired   private DataSource dataSource;   @Bean   public TokenStore tokenStore() {     return new JdbcTokenStore(dataSource);   }       @Override   public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {       endpoints.authenticationManager(authenticationManager);     endpoints.tokenStore(tokenStore());     // 配置TokenServices参数     DefaultTokenServices tokenServices = new DefaultTokenServices();     tokenServices.setTokenStore(endpoints.getTokenStore());     tokenServices.setSupportRefreshToken(false);     tokenServices.setClientDetailsService(endpoints.getClientDetailsService());     tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());     tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.MINUTES.toSeconds(10)); //分钟     endpoints.tokenServices(tokenServices);   }       @Override   public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {     oauthServer.checkTokenAccess("isAuthenticated()");     oauthServer.allowFormAuthenticationForClients();   }     @Bean   public ClientDetailsService clientDetails() {     return new JdbcClientDetailsService(dataSource);   }     @Override   public void configure(ClientDetailsServiceConfigurer clients) throws Exception {       clients.withClientDetails(clientDetails());     /*         *    基于内存配置项     *    clients.inMemory()         .withClient("community")         .secret("community")         .authorizedGrantTypes("authorization_code").redirectUris("http://tech.taiji.com.cn/")         .scopes("app").and() .withClient("dev")         .secret("dev")         .authorizedGrantTypes("authorization_code").redirectUris("http://localhost:7777/")         .scopes("app");*/   } }

客户端应用主要配置如下:

application.properties中Oauth3配置,如下

security.oauth3.client.clientId=dev security.oauth3.client.clientSecret=dev security.oauth3.client.accessTokenUri=http://localhost:9999/oauth/token security.oauth3.client.userAuthorizationUri=http://localhost:9999/oauth/authorize security.oauth3.resource.loadBalanced=true security.oauth3.resource.userInfoUri=http://localhost:9999/me security.oauth3.resource.logout.url=http://localhost:9999/revoke-token security.oauth3.default.roleName=ROLE_USER

Oauth3Config配置,授权Oauth3Sso配置和Spring Security配置,如下:

@Configuration @EnableOAuth3Sso public class Oauth3Config extends WebSecurityConfigurerAdapter{  @Autowired  CustomSsoLogoutHandler customSsoLogoutHandler;    @Autowired  OAuth3ClientContext oauth3ClientContext;    @Bean  public HttpFirewall allowUrlEncodedSlashHttpFirewall() {    StrictHttpFirewall firewall = new StrictHttpFirewall();    firewall.setAllowUrlEncodedSlash(true);    firewall.setAllowSemicolon(true);    return firewall;  }    @Bean  @ConfigurationProperties("security.oauth3.client")  public AuthorizationCodeResourceDetails taiji() {  return new AuthorizationCodeResourceDetails();  }    @Bean  public CommunitySuccessHandler customSuccessHandler() {  CommunitySuccessHandler customSuccessHandler = new CommunitySuccessHandler();  customSuccessHandler.setDefaultTargetUrl("/");  return customSuccessHandler;  }    @Bean  public CustomFailureHandler customFailureHandler() {  CustomFailureHandler customFailureHandler = new CustomFailureHandler();  customFailureHandler.setDefaultFailureUrl("/index");  return customFailureHandler;  }    @Bean  @Primary  @ConfigurationProperties("security.oauth3.resource")  public ResourceServerProperties taijiOauthorResource() {  return new ResourceServerProperties();  }    @Bean  @Override  public AuthenticationManager authenticationManagerBean() throws Exception {  List<AuthenticationProvider> authenticationProviderList = new ArrayList<AuthenticationProvider>();  authenticationProviderList.add(customAuthenticationProvider());  AuthenticationManager authenticationManager = new ProviderManager(authenticationProviderList);  return authenticationManager;  }    @Autowired  public TaijiUserDetailServiceImpl userDetailsService;    @Bean  public TaijiAuthenticationProvider customAuthenticationProvider() {  TaijiAuthenticationProvider customAuthenticationProvider = new TaijiAuthenticationProvider();  customAuthenticationProvider.setUserDetailsService(userDetailsService);  return customAuthenticationProvider;  }      @Autowired  private MenuService menuService;  @Autowired  private RoleService roleService;  @Bean  public TaijiSecurityMetadataSource taijiSecurityMetadataSource() {  TaijiSecurityMetadataSource fisMetadataSource = new TaijiSecurityMetadataSource(); // fisMetadataSource.setMenuService(menuService);  fisMetadataSource.setRoleService(roleService);  return fisMetadataSource;  }    @Autowired  private CommunityAccessDecisionManager accessDecisionManager;  @Bean  public CommunityFilterSecurityInterceptor communityfiltersecurityinterceptor() throws Exception {  CommunityFilterSecurityInterceptor taijifiltersecurityinterceptor = new CommunityFilterSecurityInterceptor();  taijifiltersecurityinterceptor.setFisMetadataSource(taijiSecurityMetadataSource());  taijifiltersecurityinterceptor.setAccessDecisionManager(accessDecisionManager);  taijifiltersecurityinterceptor.setAuthenticationManager(authenticationManagerBean());  return taijifiltersecurityinterceptor;  }      @Override  protected void configure(HttpSecurity http) throws Exception {  http.authorizeRequests() //     .antMatchers("/").permitAll()  //  .antMatchers("/login").permitAll() //  //  .antMatchers("/image").permitAll() //  //  .antMatchers("/upload/*").permitAll() // for //  .antMatchers("/common/**").permitAll() // for  //  .antMatchers("/community/**").permitAll()    //     .antMatchers("/").anonymous()      .antMatchers("/personal/**").authenticated()      .antMatchers("/notify/**").authenticated()      .antMatchers("/admin/**").authenticated()      .antMatchers("/manage/**").authenticated()      .antMatchers("/**/personal/**").authenticated()      .antMatchers("/user/**").authenticated()   .anyRequest()   .permitAll() //  .authenticated()   .and()   .logout()   .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))   .addLogoutHandler(customSsoLogoutHandler)   .deleteCookies("JSESSIONID").invalidateHttpSession(true)   .and()   .csrf().disable()   //.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   //.and()   .addFilterBefore(loginFilter(), BasicAuthenticationFilter.class)   .addFilterAfter(communityfiltersecurityinterceptor(), FilterSecurityInterceptor.class);///TaijiSecurity权限控制  }    @Override  public void configure(WebSecurity web) throws Exception {  // 解决静态资源被拦截的问题  web.ignoring().antMatchers("/theme/**")   .antMatchers("/community/**")   .antMatchers("/common/**")   .antMatchers("/upload/*");  web.httpFirewall(allowUrlEncodedSlashHttpFirewall());  }      public OAuth3ClientAuthenticationProcessingFilter loginFilter() throws Exception {  OAuth3ClientAuthenticationProcessingFilter ff = new OAuth3ClientAuthenticationProcessingFilter("/login");  OAuth3RestTemplate restTemplate = new OAuth3RestTemplate(taiji(),oauth3ClientContext);  ff.setRestTemplate(restTemplate);  UserInfoTokenServices tokenServices = new UserInfoTokenServices(taijiOauthorResource().getUserInfoUri(), taiji().getClientId());  tokenServices.setRestTemplate(restTemplate);  ff.setTokenServices(tokenServices);  ff.setAuthenticationSuccessHandler(customSuccessHandler());     ff.setAuthenticationFailureHandler(customFailureHandler());  return ff;  } }

授权成功回调类,认证成功用户落地,如下:

public class CommunitySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {    protected final Log logger = LogFactory.getLog(this.getClass());    private RequestCache requestCache = new HttpSessionRequestCache();  @Autowired  private UserService userService;  @Autowired  private RoleService roleService;  @Inject  AuthenticationManager authenticationManager;  @Value("${security.oauth3.default.roleName}")  private String defaultRole;  @Inject  TaijiOperationLogService taijiOperationLogService;    @Inject  CommunityConfiguration communityConfiguration;    @Inject  private ObjectMapper objectMapper;    @ScoreRule(code="login_score")  @Override  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,   Authentication authentication) throws ServletException, IOException {  // 存放authentication到SecurityContextHolder  SecurityContextHolder.getContext().setAuthentication(authentication);  HttpSession session = request.getSession(true);  // 在session中存放security context,方便同一个session中控制用户的其他操作  session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());  OAuth3Authentication oauth3Authentication = (OAuth3Authentication) authentication;  Object details = oauth3Authentication.getUserAuthentication().getDetails();  UserDto user = saveUser((Map) details);//用户落地  Collection<GrantedAuthority> obtionedGrantedAuthorities = obtionGrantedAuthorities(user);  UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(   new User(user.getLoginName(), "", true, true, true, true, obtionedGrantedAuthorities),    authentication.getCredentials(), obtionedGrantedAuthorities);    newToken.setDetails(details);  Object oath3details=oauth3Authentication.getDetails();  oauth3Authentication = new OAuth3Authentication(oauth3Authentication.getOAuth3Request(), newToken);  oauth3Authentication.setDetails(oath3details);  oauth3Authentication.setAuthenticated(true);  SecurityContextHolder.getContext().setAuthentication(oauth3Authentication);    LogUtil.log2database(taijiOperationLogService, request, user.getLoginName(), "user", "", "", "user_login", "登录", "onAuthenticationSuccess","");  session.setAttribute("user", user);  Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();    SavedRequest savedRequest = requestCache.getRequest(request, response);  if (savedRequest == null) {   super.onAuthenticationSuccess(request, response, authentication);   return;  }  String targetUrlParameter = getTargetUrlParameter();  if (isAlwaysUseDefaultTargetUrl()   || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {   requestCache.removeRequest(request, response);   super.onAuthenticationSuccess(request, response, authentication);   return;  }  clearAuthenticationAttributes(request);  // Use the DefaultSavedRequest URL  String targetUrl = savedRequest.getRedirectUrl(); // logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);    logger.debug("Redirecting to last savedRequest Url: " + targetUrl);  getRedirectStrategy().sendRedirect(request, response, targetUrl); // getRedirectStrategy().sendRedirect(request, response, this.getDefaultTargetUrl());  }    public void setRequestCache(RequestCache requestCache) {  this.requestCache = requestCache;  }    //用户落地  private UserDto saveUser(Map userInfo) {  UserDto dto=null;  try {   String json = objectMapper.writeValueAsString(userInfo);   dto = objectMapper.readValue(json,UserDto.class);  } catch (JsonProcessingException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } catch (IOException e) {   // TODO Auto-generated catch block   e.printStackTrace();  }    UserDto user=userService.findByLoginName(dto.getLoginName());  if(user!=null) {   return user;  }  Set<RoleDto> roles= new HashSet<RoleDto>();  RoleDto role = roleService.findByRoleName(defaultRole);  roles.add(role);  dto.setRoles(roles);  List<UserDto> list = new ArrayList<UserDto>();  list.add(dto);  dto.generateTokenForCommunity(communityConfiguration.getControllerSalt());  String id =userService.saveUserWithRole(dto,communityConfiguration.getControllerSalt());  dto.setId(id);  return dto;  }    /**    * Map转成实体对象    *    * @param map  map实体对象包含属性    * @param clazz 实体对象类型    * @return    */   public static <T> T map2Object(Map<String, Object> map, Class<T> clazz) {     if (map == null) {       return null;     }     T obj = null;     try {       obj = clazz.newInstance();         Field[] fields = obj.getClass().getDeclaredFields();       for (Field field : fields) {         int mod = field.getModifiers();         if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {           continue;         }         field.setAccessible(true);         String filedTypeName = field.getType().getName();         if (filedTypeName.equalsIgnoreCase("java.util.date")) {           String datetimestamp = String.valueOf(map.get(field.getName()));           if (datetimestamp.equalsIgnoreCase("null")) {             field.set(obj, null);           } else {             field.set(obj, new Date(Long.parseLong(datetimestamp)));           }         } else {          String v = map.get(field.getName()).toString();           field.set(obj, map.get(field.getName()));         }       }     } catch (Exception e) {       e.printStackTrace();     }     return obj;   }        // 取得用户的权限  private Collection<GrantedAuthority> obtionGrantedAuthorities(UserDto users) {  Collection<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();  // 获取用户角色  Set<RoleDto> roles = users.getRoles();  if (null != roles && !roles.isEmpty())   for (RoleDto role : roles) {  authSet.add(new SimpleGrantedAuthority(role.getId()));   }  return authSet;  } }

客户端应用,单点登录方法,如下:

@RequestMapping(value = "/loadToken", method = { RequestMethod.GET })  public void loadToken(Model model,HttpServletResponse response,@RequestParam(value = "clientId", required = false) String clientId) {  String token = "";  RequestAttributes ra = RequestContextHolder.getRequestAttributes();  ServletRequestAttributes sra = (ServletRequestAttributes) ra;  HttpServletRequest request = sra.getRequest();  HttpSession session = request.getSession();  if (session.getAttribute("SPRING_SECURITY_CONTEXT") != null) {   SecurityContext securityContext = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");   Authentication authentication = securityContext.getAuthentication();   OAuth3AuthenticationDetails OAuth3AuthenticationDetails = (OAuth3AuthenticationDetails) authentication.getDetails();   token = OAuth3AuthenticationDetails.getTokenValue();  }  try {   String url = "http://localhost:9999/rediect?clientId=dev&token="+token;   response.sendRedirect(url);  } catch (IOException e) {   e.printStackTrace();  }  }

服务端应用,单点登录方法,如下:

@RequestMapping("/rediect")  public String rediect(HttpServletResponse responsel, String clientId, String token) {  OAuth3Authentication authentication = tokenStore.readAuthentication(token);  if (authentication == null) {   throw new InvalidTokenException("Invalid access token: " + token);  }  OAuth3Request request = authentication.getOAuth3Request();  Map map = new HashMap();  map.put("code", request.getRequestParameters().get("code"));  map.put("grant_type", request.getRequestParameters().get("grant_type"));  map.put("response_type", request.getRequestParameters().get("response_type"));  //TODO 需要查询一下要跳转的Client_id配置的回调地址  map.put("redirect_uri", "http://127.0.0.1:8888");  map.put("client_id", clientId);  map.put("state", request.getRequestParameters().get("state"));  request = new OAuth3Request(map, clientId, request.getAuthorities(), request.isApproved(), request.getScope(),   request.getResourceIds(), map.get("redirect_uri").toString(), request.getResponseTypes(),request.getExtensions()); // 模拟用户登录  Authentication t = tokenStore.readAuthentication(token);  OAuth3Authentication auth = new OAuth3Authentication(request, t);  OAuth3AccessToken new_token = defaultTokenServices.createAccessToken(auth);  return "redirect:/user_info?access_token=" + new_token.getValue();  } @RequestMapping({ "/user_info" })  public void user(String access_token,HttpServletResponse response) {  OAuth3Authentication auth=tokenStore.readAuthentication(access_token);  OAuth3Request request=auth.getOAuth3Request();   Map<String, String> map = new LinkedHashMap<>();   map.put("loginName", auth.getUserAuthentication().getName());   map.put("password", auth.getUserAuthentication().getName());   map.put("id", auth.getUserAuthentication().getName());   try {  response.sendRedirect(request.getRedirectUri()+"?name="+auth.getUserAuthentication().getName());  } catch (IOException e) {  e.printStackTrace();  } }

个人总结

Oauth3的设计相对复杂,需要深入学习多看源码才能了解内部的一些规则,如数据token的存储是用的实体序列化后内容,需要反序列才能在项目是使用,也许是为了安全,但在学习过程需要提前掌握,还有在token的过期时间不能为0,通常来讲过期时间为0代表长期有效,但在Oauth3中则报错,这些坑需要一点点探索。

通过集成Spring Security和Oauth3较大的提供的开发的效率,也提供的代码的灵活性和可用性。但封装的核心类需要大家都了解一下,通读下代码,以便在项目中可随时获取需要的参数。

示例代码

以下是个人的一套代码,供参考。

基于Spring Cloud的微服务框架集成Oauth3的代码示例

Oauth3数据结构,如下:

怎么在Spring Security中实现Oauth2授权怎么在Spring Security中实现Oauth2授权怎么在Spring Security中实现Oauth2授权怎么在Spring Security中实现Oauth2授权怎么在Spring Security中实现Oauth2授权

关于怎么在Spring Security中实现Oauth2授权就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI