大家好,欢迎来到IT知识分享网。
文章目录
一、 CAS简介
Cas的全称是Centeral Authentication Service,是对单点登录SSO(Single Sign On)
的一种实现。其由Cas Server和Cas Client两部分组成,Cas Server是核心,而Cas Client通常就对应于我们的应用。一个Cas Server可以对应于多个Cas Client。它允许我们在一个Client进行登录以后无需再让用户输入用户名和密码进行认证即可访问其它Client应用。
Cas Server的主要作用是通过发行和验证Ticket(票)来对用户进行认证和授权访问Client应用,用于认证的凭证信息都是由Cas Server管理的。而Cas Client就对应于我们真正的应用,当然其中会使用到Cas相关的类,用于与Cas Server进行交互。
二、 CAS认证流程
下图是官网上用户认证的流程图
用户首次访问应用app1
如你所见,在第一次访问应用app1时,由于没有登录会直接跳转到Cas Server去进行登录认证,此时将附带查询参数service在Cas Server的登录地址上,表示登录成功后将要跳转的地址。
此时Cas Server检查到没有之前成功登录后生成的SSO Session信息,那么就会引导用户到登录页面进行登录。如:
https://cas.example.com:8443/cas/login?service=http%3A%2F%2Fapp.example.com%3A8080%2Flogin%2Fcas
用户输入信息提交登录请求,Cas Server认证成功后将生成对应的SSO Session,以及名为CASTGC的cookie,该cookie包含用来确定用户SSO Session的Ticket Granting Ticket(TGT)。之后会生成一个Service Ticket(ST),并将以ticket作为查询参数名,以该ST作为查询参数值跳转到登录时service对应的URL。如:
http://app.example.com:8080/login/cas?ticket=ST-7-hXaLlWKHnwc-RpxHEyVf0aQVF0wxxxxxx
之后的操作对用户来说都是透明的,即不可见的。
app1之后将以service和ticket作为查询参数请求Cas Server对service进行验证,地址举例:
https://cas.example.com:8443/cas/p3/serviceValidate?ticket=ST-15-K7SRE776Y1e4LtJqcMOzzaehcbgmaruilei&service=http%3A%2F%2Fapp.example.com%3A8080%2Flogin%2Fcas
验证通过后Cas Server将返回当前用户的用户名等信息。app1就会给当前用户生成其自身的Session,以后该用户以该Session都可以成功的访问app1,而不需要再去请求Cas Server进行认证。流程图如下
第二次访问app1
当该用户再去访问app2的时候,由于其在app2上没有对应的Session信息,将会跳转到Cas Server的登录地址,Cas Server此时发现其包含名为CASTGC的cookie,将获取其中包含的TGT来获取对应的SSO Session,然后会将用户重定向到app2对应的地址,以Service Ticket作为查询参数。之后app2会向Cas Server发送请求校验该Service Ticket,校验成功后app2将建立该用户对应的Session信息,以后该用户以该Session就可以自由的访问app2了。
综上所述,我们知道,各系统之间的单点登录是通过Cas Server生成的SSO Session来交流的,而用户与实际的应用系统进行交互的时候,各应用系统将建立单独的Session,以满足用户与该系统的交互需求。
三、 接入CAS所需配置
pom.xml引入文件配置
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
application.yml配置
cas:
server:
prefix: https://cas.example.com:8443/cas
login: /login
logout: /logout
client:
prefix: http://127.0.0.1:8080/clientapp
login: /login/cas
logout: /logout/cas
cas相关过滤器、入口配置
@Configuration
public class CasSecurityProperties {
@Value("${cas.server.prefix}")
private String casServerPrefix;
@Value("${cas.server.login}")
private String casServerLogin;
@Value("${cas.server.logout}")
private String casServerLogout;
@Value("${cas.client.prefix}")
private String casClientPrefix;
@Value("${cas.client.login}")
private String casClientLogin;
@Value("${cas.client.logout}")
private String casClientLogout;
/** * 设置本cas服务的属性bean * serviceProperties.setAuthenticateAllArtifacts(true); * 设置带有ticket参数的请求都可以触发认证,主要在代理认证时使用 * @return */
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casClientPrefix + casClientLogin);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/** * 票据验证,使用cas3.0 代理票据认证器 * 传递cas server地址进行初始化 */
@Bean
public Cas30ProxyTicketValidator ticketValidator() {
Cas30ProxyTicketValidator validator = new Cas30ProxyTicketValidator(casServerPrefix);
return validator;
}
/** * 接收cas服务器发出的注销请求 */
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casServerPrefix);
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new MyLogoutSuccessHandler();
}
/** * 注销请求转发到cas server * 由于登出会重定向到cas server页面,发生跨域, * 此处重写了LogoutSuccessHandler,后台只进行session销毁,然后通知前端进行登出跳转 */
@Bean
public LogoutFilter logoutFilter(LogoutSuccessHandler logoutSuccessHandler) {
LogoutFilter logoutFilter = new LogoutFilter(logoutSuccessHandler, new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casClientLogout);
return logoutFilter;
}
@Bean
public UserDetailsService userDetailsService() {
UserServiceImpl userService = new UserServiceImpl();
return userService;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider(ServiceProperties sp, TicketValidator ticketValidator, UserDetailsService userDetailsService) {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setKey("casProvider");
casAuthenticationProvider.setServiceProperties(sp);
casAuthenticationProvider.setTicketValidator(ticketValidator);
casAuthenticationProvider.setUserDetailsService(userDetailsService);
return casAuthenticationProvider;
}
/** * cas filter类,针对/login/cas请求做用户认证 * filter.setAuthenticationDetailsSource(new ServiceAuthenticationDetailsSource(serviceProperties)); * 该设置可以在认证信息中记录请求原始url,不再使用spring security默认的 /login/cas * * @param serviceProperties * @param provider * @return */
@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties serviceProperties,
AuthenticationProvider provider) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties);
filter.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
return filter;
}
/** * filter入口,未登录时跳转到cas server * @param serviceProperties * @return */
@Bean
@Primary
public CasAuthenticationEntryPoint authenticationEntryPoint(ServiceProperties serviceProperties) {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(casServerPrefix + casServerLogin);
entryPoint.setServiceProperties(serviceProperties);
return entryPoint;
}
其中UserDetailsServiceImpl是需要用户自己实现的类,可以在其中给登录用户配置权限角色相关信息,实现举例:
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
List<GrantedAuthority> authorities = new ArrayList<>();
//去数据库查询用户 及相关权限
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(username, "N/A", true, true, true, true, authorities);
}
}
总入口配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private AuthenticationProvider authenticationProvider;
@Resource
private CasAuthenticationFilter casAuthenticationFilter;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Resource
private LogoutFilter logoutFilter;
@Resource
private SingleSignOutFilter singleSignOutFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
/** * 配置放行的资源 * @param web * @throws Exception */
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/fonts/**", "/css/**", "/favicon.ico","/test.html");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.anyRequest().authenticated().and()
.logout().permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.addFilter(casAuthenticationFilter)
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
.addFilterBefore(logoutFilter, LogoutFilter.class);
}
四、 CAS代理认证(CAS Proxy)
考虑这样一种场景:有两个应用App1和App2,它们都是受Cas Server保护的,即请求它们时都需要通过Cas Server的认证。现需要在App1中通过Http请求访问App2,显然该请求将会被App2配置的Cas的AuthenticationFilter拦截并转向Cas Server,Cas Server将引导用户进行登录认证,这样我们也就不能真正的访问到App2了。针对这种应用场景,Cas也提供了对应的支持。
CAS代理模式原理
Cas Proxy可以让我们轻松的通过App1访问App2时通过Cas Server的认证,从而访问到App2。
其主要原理是这样的,App1先通过Cas Server的认证,然后向Cas Server申请一个针对于App2的proxy ticket,之后在访问App2时把申请到的针对于App2的proxy ticket以参数ticket传递过去。App2的AuthenticationFilter将拦截到该请求,发现该请求携带了ticket参数后将放行交由后续的Ticket Validation Filter处理。Ticket Validation Filter将会传递该ticket到Cas Server进行认证,显然该ticket是由Cas Server针对于App2发行的,App2在申请校验时是可以校验通过的,这样我们就可以正常的访问到App2了。
同样,在App1代理访问App2认证成功后,在回话过期前再次访问App2,可以直接获取到数据,不用再次去cas server认证。
官网代理认证流程图:
五、 CAS代理配置
代理端和被代理端配置略有不同,如果一个服务同时扮演两种角色,可以一起配置
1)代理端配置
这里只需要在cas认证的基础上增加几个代理的配置,包括给票据认证器ticketValidator设置PGT存储容器,代理回调地址——绝对地址,认证过滤器CASAuthenticationFilter设置代理拦截地址—相对地址
/** * 票据验证,使用cas3.0 代理票据认证器 * 传递cas server地址进行初始化 */
@Bean
public Cas30ProxyTicketValidator ticketValidator(ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
Cas30ProxyTicketValidator validator = new Cas30ProxyTicketValidator(casServerPrefix);
validator.setProxyGrantingTicketStorage(proxyGrantingTicketStorage);
validator.setProxyCallbackUrl(casClientPrefix + "/proxyCallback");
return validator;
}
@Bean
public ProxyGrantingTicketStorage proxyGrantingTicketStorage() {
return new ProxyGrantingTicketStorageImpl();
}
/** * cas filter类,针对/login/cas请求做用户认证 * @param serviceProperties * @param provider * @return */
@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties serviceProperties,
ProxyGrantingTicketStorage proxyGrantingTicketStorage,
AuthenticationProvider provider) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties);
filter.setProxyReceptorUrl("/proxyCallback");
filter.setProxyGrantingTicketStorage(proxyGrantingTicketStorage);
filter.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
return filter;
}
2)被代理端配置
/** * 设置cas属性bean * serviceProperties.setAuthenticateAllArtifacts(true); * 设置带有ticket参数的请求都可以触发认证,主要在代理认证时使用 * @return */
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casClientPrefix + casClientLogin);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/** * 票据验证,使用cas3.0 * 打开接收任何代理开关,这样可以接受任何代理,不太安全 * 还可以设置代理链,指定允许代理的服务 */
@Bean
public Cas30ProxyTicketValidator ticketValidator() {
Cas30ProxyTicketValidator validator = new Cas30ProxyTicketValidator(casServerPrefix);
validator.setAcceptAnyProxy(true);
//设置代理链方式
// validator.setAllowedProxyChains(new ProxyList());
return validator;
}
/** 1. cas filter类,针对/login/cas请求做用户认证 2. filter.setAuthenticationDetailsSource(new ServiceAuthenticationDetailsSource(serviceProperties)); 3. 该设置可以在认证信息中记录请求原始url,不再使用spring security默认的 /login/cas */
@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties serviceProperties,
AuthenticationProvider provider) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties);
filter.setAuthenticationDetailsSource(new ServiceAuthenticationDetailsSource(serviceProperties));
filter.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
return filter;
}
六、 CAS 系统中的票据:TGC、TGT 、ST 、PGT 、PGTIOU 、PT
票据名称 | 解释 |
---|---|
TGT(Ticket Grangting Ticket) | TGT是CAS为用户签发的登录票据,拥有了TGT,用户就可以证明自己在CAS成功登录过。用户在CAS认证成功后,CAS生成Cookie(叫TGC),写入浏览器,同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是Cookie的值。当HTTP再次请求到来时,如果传过来的有CAS生成的Cookie,则CAS以此Cookie值为key查询缓存中有无TGT ,如果有的话,则说明用户之前登录过,如果没有,则用户需要重新登录。 |
TGC(ticket-granting cookie | 授权的票据证明,由 CAS Server 通过 SSL 方式发送给终端用户,存放用户身份认证凭证的Cookie。在浏览器和CAS Server间通讯时使用,并且只能基于安全通道传输(Https),是CAS Server用来明确用户身份的凭证。 |
ST(Service Ticket) | ST是CAS为用户签发的访问某一service的票据。用户访问service时,service发现用户没有ST,则要求用户去CAS获取ST。用户向CAS发出获取ST的请求,如果用户的请求中包含Cookie,则CAS会以此Cookie值为key查询缓存中有无TGT,如果存在TGT,则用此TGT签发一个ST,返回给用户。用户凭借ST去访问service,service拿ST去CAS验证,验证通过后,允许用户访问资源。 |
PGT(Proxy Granting Ticket) | Proxy Service的代理凭据。用户通过CAS成功登录某一Proxy Service后,CAS生成一个PGT对象,缓存在CAS本地,同时将PGT的值(一个UUID字符串)回传给Proxy Service,并保存在Proxy Service里。Proxy Service拿到PGT后,就可以为Target Service(back-end service)做代理,为其申请PT。 |
PGTIOU(Proxy Granting Ticket I Owe You) | PGTIOU是CAS协议中定义的一种附加票据,它增强了传输、获取PGT的安全性。PGT的传输与获取的过程:Proxy Service调用CAS的serviceValidate接口验证ST成功后,CAS首先会访问pgtUrl指向的Https URL,将生成的 PGT及PGTIOU传输给proxy service,proxy service会以PGTIOU为key,PGT为value,将其存储在Map中;然后CAS会生成验证ST成功的XML消息,返回给Proxy Service,XML消息中含有PGTIOU,proxy service收到XML消息后,会从中解析出PGTIOU的值,然后以其为key,在Map中找出PGT的值,赋值给代表用户信息的Assertion对象的pgtId,同时在Map中将其删除。 |
PT(Proxy Ticket) | PT是用户访问Target Service(back-end service)的票据。如果用户访问的是一个Web应用,则Web应用会要求浏览器提供ST,浏览器就会用Cookie去CAS获取一个ST,然后就可以访问这个Web应用了。如果用户访问的不是一个Web应用,而是一个C/S结构的应用,因为C/S结构的应用得不到Cookie,所以用户不能自己去CAS获取ST,而是通过访问proxy service的接口,凭借proxy service的PGT去获取一个PT,然后才能访问到此应用。 |
七、 未使用Spring Security的配置
若未使用Security,可以直接引入cas-client包,相关配置步骤基本一致
pom.xml中引入jar包
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.1</version>
</dependency>
相应application.properties配置
# 监听退出的接口,即所有接口都会进行监听
spring.cas.sign-out-filters=/*
# 需要拦截的认证的接口
spring.cas.auth-filters=/*
spring.cas.validate-filters=/*
spring.cas.request-wrapper-filters=/*
spring.cas.assertion-filters=/*
# 表示忽略拦截的接口,也就是不用进行拦截
spring.cas.ignore-filters=/test
spring.cas.cas-server-login-url=https://cas.hanshow.com:8443/cas/login
spring.cas.cas-server-url-prefix=https://cas.hanshow.com:8443/cas/
spring.cas.redirect-after-validation=true
spring.cas.use-session=true
spring.cas.server-name=http://localhost:9443
logging.level.web=debug
logging.level.sql=debug
logging.level.root=debug
以上配置信息注入到java bean——SpringCasAutoConfig中,在过滤器配置中使用
CAS过滤器配置
@Configuration
@Component
public class CasCustomConfig {
@Resource
SpringCasAutoconfig autoconfig;
private static boolean casEnabled = true;
public CasCustomConfig() {
}
@Bean
public SpringCasAutoconfig getSpringCasAutoconfig() {
return new SpringCasAutoconfig();
}
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listener = new ServletListenerRegistrationBean<SingleSignOutHttpSessionListener>();
listener.setEnabled(casEnabled);
listener.setListener(new SingleSignOutHttpSessionListener());
listener.setOrder(1);
return listener;
}
/** * 该过滤器用于实现单点登出功能,单点退出配置,一定要放在其他filter之前 * * @return */
@Bean
public FilterRegistrationBean singleSignOutFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new SingleSignOutFilter());
filterRegistration.setEnabled(casEnabled);
if (autoconfig.getSignOutFilters().size() > 0) {
filterRegistration.setUrlPatterns(autoconfig.getSignOutFilters());
} else {
filterRegistration.addUrlPatterns("/*");
}
filterRegistration.addInitParameter("casServerUrlPrefix", autoconfig.getCasServerUrlPrefix());
filterRegistration.setOrder(3);
return filterRegistration;
}
/** * 该过滤器负责用户的认证工作 * * @return */
@Bean
public FilterRegistrationBean authenticationFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new AuthenticationFilter());
filterRegistration.setEnabled(casEnabled);
if (autoconfig.getAuthFilters().size() > 0) {
filterRegistration.setUrlPatterns(autoconfig.getAuthFilters());
} else {
filterRegistration.addUrlPatterns("/*");
}
if (autoconfig.getIgnoreFilters() != null) {
filterRegistration.addInitParameter("ignorePattern", autoconfig.getIgnoreFilters());
}
filterRegistration.addInitParameter("casServerLoginUrl", autoconfig.getCasServerLoginUrl());
filterRegistration.addInitParameter("serverName", autoconfig.getServerName());
filterRegistration.addInitParameter("useSession", autoconfig.isUseSession() ? "true" : "false");
filterRegistration.addInitParameter("redirectAfterValidation", autoconfig.isRedirectAfterValidation() ? "true" : "false");
filterRegistration.setOrder(4);
return filterRegistration;
}
/** * 该过滤器负责对Ticket的校验工作,使用CAS 3.0协议 * * @return */
@Bean
public FilterRegistrationBean cas30ProxyReceivingTicketValidationFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
filterRegistration.setEnabled(casEnabled);
if (autoconfig.getValidateFilters().size() > 0) {
filterRegistration.setUrlPatterns(autoconfig.getValidateFilters());
} else {
filterRegistration.addUrlPatterns("/*");
}
filterRegistration.addInitParameter("casServerUrlPrefix", autoconfig.getCasServerUrlPrefix());
filterRegistration.addInitParameter("serverName", autoconfig.getServerName());
filterRegistration.setOrder(5);
return filterRegistration;
}
@Bean
public FilterRegistrationBean httpServletRequestWrapperFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new HttpServletRequestWrapperFilter());
filterRegistration.setEnabled(true);
if (autoconfig.getRequestWrapperFilters().size() > 0) {
filterRegistration.setUrlPatterns(autoconfig.getRequestWrapperFilters());
} else {
filterRegistration.addUrlPatterns("/*");
}
filterRegistration.setOrder(6);
return filterRegistration;
}
/** * 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 * 比如AssertionHolder.getAssertion().getPrincipal().getName()。 * 这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息 * * @return */
@Bean
public FilterRegistrationBean assertionThreadLocalFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new AssertionThreadLocalFilter());
filterRegistration.setEnabled(true);
if (autoconfig.getAssertionFilters().size() > 0) {
filterRegistration.setUrlPatterns(autoconfig.getAssertionFilters());
} else {
filterRegistration.addUrlPatterns("/*");
}
filterRegistration.setOrder(7);
return filterRegistration;
}
}
根据以上配置,运行项目后,可以获取到登录的用户信息
@RestController
public class TestController {
@RequestMapping("/getName")
public Object getName(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Assertion assertion = (Assertion) session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
return assertion.getPrincipal().getName();
}
八、 可能遇到的问题
1) Host配置
要成功访问CAS server,需要在本地host配置CAS server的域名映射
192.168.3.124 cas.hanshow.com
2) 证书问题
开发中,在首次访问CAS server时,会报:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building
failed:……
Java在请求某些不受信任的https网站时是会发生这种情况
两种解决方案:
- 手动导入证书到本地证书库
- 信任所有SSL证书
目前项目采用的第一种,详细步骤可参照博文:
https://blog.csdn.net/gabriel576282253/article/details/81531746
3) 跨域问题
在登录和登出时,都会发生302重定向,若前端调用后端接口发起重定向,会有跨域问题(毕竟要跳转到其他域名)
目前采用的解决方案:
- 部署时将前端文件放于后端项目根目录,在访问时Spring Security会拦截请求自动跳转到CAS server,可解决登录跨域
- 登出时是通过接口调用,后端只做session销毁,然后返回,前端完成登出跳转
以上是在使用Spring Security情况下,接入cas客户端所需配置。
用户登录后,还需要自定义资源权限相关的实现。
本文参考
- https://blog.csdn.net/elim168/article/details/43560737
- https://blog.csdn.net/Anumbrella/article/details/80821486
他们的是cas系列博客,对于cas的理解和使用有很大帮助,在此表示感谢
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/15417.html