百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

用户统一认证SSO解决方案(CAS)(sso认证原理)

toyiye 2024-09-03 22:35 4 浏览 0 评论

介绍

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

CAS( Central Authentication Service)是耶鲁大学的开源项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法。

CAS 开始于 2001 年, 并在 2004 年 12 月正式成为 JA-SIG 的一个项目。

编写目的

本文档作为sodb应用系统认证集成手册,旨在指导sodb集成系统实现统一身份认证和登录集成。

包括了SSL安全证书认证、CAS客户端配置。

术语与缩略语

整体认证流程

流程说明:

1、app端有session时,无需再次请求cas服务端(实线部分的流程即可)。

2、app端有st,则直接校验即可(无须重定向到cas服务端获取st)。

3、只有当1、2都不满足,或者校验不通过时,才会唤起输入用户名和密码的页面。

4、cas是集成了spring-webflow的,而对应的信息又保存在cas_server端的session中。所以要分布式部署的话,不能忘了session的共享。

票据说明:

1、TGC (Ticket-granting cookie),是存放在浏览器中的,从它是名字就能判断出来。

2、包含用户信息的session保存在app服务端和cas的服务端

3、TGT(Ticket-granting Ticket),ST(Service Ticket)存放在cas服务端中。要实现上图的分布式部署,将其存入共用的数据库即可。

单点登录实现方案

单点登录概述

单点登录,即用户只登录一次,在其随后访问所有的授权的资源时,都不需要再次登录。本方案使用CAS产品为基础来实现。

CAS(Central Authentication Service) 是 Yale 大学发起的一个开源项目,据统计,大概每 10 个采用开源构建 Web SSO 的 Java 项目,就有 8 个使用 CAS 。下方主要介绍CAS结构体系、协议与安全性。

CAS结构体系

从结构体系看, CAS 包含两部分:

1) CAS Server

CAS Server 负责完成对用户的认证工作, CAS Server 需要独立部署。

CAS Server 会处理用户名 / 密码等凭证 (Credentials) ,可通过连接到LADP检索用户名密码信息,也可能在 XML 文件中检索用户密码,对这种方式, CAS 均提供一种灵活但统一的接口 / 实现分离的方式, CAS 究竟是用何种认证方式,跟 CAS 协议是分离的,也就是,这个认证的实现细节可以自己定制和扩展。

2) CAS Client

CAS Client 负责部署在客户端(注意,我是指 Web 应用),原则上, CAS Client 的部署意味着,当有对本地 Web 应用的受保护资源的访问请求,并且需要对请求方进行身份认证, Web 应用不再接受任何的用户名密码等类似的 Credentials ,而是重定向到 CAS Server 进行认证。

CAS协议

1) 基础协议

CAS 基础模式

上图是一个最基础的 CAS 协议, CAS Client 以 Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同时, CAS Client 会分析 HTTP 请求中是否包请求 Service Ticket( 上图中的 Ticket) ,如果没有,则说明该用户是没有经过认证的,于是, CAS Client 会重定向用户请求到 CAS Server ( Step 2 )。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials , CAS Server 会产生一个随机的 Service Ticket ,然后,缓存该 Ticket ,并且重定向用户到 CAS Client (附带刚才产生的 Service Ticket ), Service Ticket 是不可以伪造的,最后, Step 5 和 Step6 是 CAS Client 和 CAS Server 之间完成了一个对用户的身份核实,用 Ticket 查到 Username ,因为 Ticket 是 CAS Server 产生的,因此,所以 CAS Server 的判断是毋庸置疑的。

该协议完成了一个很简单的任务,就是 User(ah.zhangsan) 打开 IE ,直接访问 某一应用A,它被立即重定向到 CAS Server 进行认证, User 可能感觉到浏览器在 应用A 和 casserver 之间重定向,但 User 是看不到, CAS Client 和 CAS Server 相互间的 Service Ticket 核实 (Validation) 过程。当 CAS Server 告知 CAS Client 用户 Service Ticket 对应确凿身份, CAS Client 才会对当前 Request 的用户进行服务。

2) CAS 如何实现 SSO

CAS 可以很简单的实现跨域的 SSO ,因为,单点被控制在CAS Server ,用户最有价值的 TGC-Cookie 只是跟 CAS Server 相关, CAS Server 就只有一个,因此,解决了 cookies 不能跨域的问题。

1.如果 User 的持有 TGC 且其还没失效,那么就进行基础协议图的 Step4 ,达到了 SSO 的效果。

2.如果 TGC 失效,那么用户还是要重新认证进行基础协议图的 Step3。

CAS安全性

CAS 的安全性是一个非常重要的 Topic 。 CAS 从 v1 到 v3 ,都很依赖于 SSL ,它假定了这样一个事实,用户在一个非常不安全的网络环境中使用 SSO , Hacker 的 Sniffer 会很容易抓住所有的 Http Traffic ,包括通过 Http 传送的密码甚至 Ticket 票据。

2.3.1 TGC/PGT 安全性

对于一个 CAS 用户来说,最重要是要保护它的 TGC ,如果 TGC 不慎被 CAS Server 以外的实体获得, Hacker 能够找到该 TGC ,然后冒充 CAS 用户访问所有授权资源。

SSO 的安全性问题比普通应用的安全性还要严重,因为 SSO 存在一种门槛效应。以前即使 Hacker 能够截获用户在 Web 应用 A 的密码,它也未必能知道用户在 Web 应用 B 的密码,但 SSO 让 Hacker 只需要截获 TGC( 突破了门槛 ) ,即能访问所有与该用户相关的所有应用系统。

PGT 跟 TGC 的角色是一样的,如果被 Hacker 获得,后果可想而知。

从基础模式可以看出, TGC 是 CAS Server 通过 SSL 方式发送给终端用户,因此,要截取 TGC 难度非常大,从而确保 CAS 的安全性。 所以CAS 的传输安全性依赖于SSL 。

2.3.2 Service Ticket/Proxy Ticket 安全性

Service Ticket 是通过 Http 传送的,意味着所网络中的其他人可以 Sniffer 到其他人的Ticket 。

CAS 协议从几个方面让 Service Ticket 变得更加安全。

· Service Ticket 只能使用一次

CAS 协议规定,无论 Service Ticket 验证是否成功, CAS Server 都会将服务端的缓存中清除该 Ticket,从而可以确保一个 Service Ticket 不被使用两次。

· Service Ticket 在一段时间内失效。

假设用户拿到 Service Ticket 之后,他请求 helloservice 的过程又被中断了, Service Ticket 就被空置了,事实上,此时, Service Ticket 仍然有效。 CAS 规定 Service Ticket 只能存活一定的时间,然后 CAS Server 会让它失效。通过在 web.xml 中配置下面的参数,可以让 Service Ticket 在多少秒内失效。该参数在业务应用的条件范围内,越小越安全。

· Service Ticket 是基于随机数生成的

Service Ticket 必须足够随机,如果 Service Ticket 生成规则被猜出(如果你使用了 ST+Helloservice+ 自增序列的方式, Hacker 就可以构造下一个 Ticket ),Hacker 就等于绕过 CAS 认证,直接访问所有服务。

CAS客户端配置

单点登录客户端集成,主要也就是配置了四个过滤器,和一个监听器。并且我们需要将用户信息和各个子系统进行同步。同时单点登录服务端配置了SSL,需要各个子系统导入证书到JDK中。

证书

证书文件:casserver.cer

导入证书

方式一:

keytool -import -trustcacerts -alias casserver -file E:\Cas-SSO\ssl\casserver.cer -keystore "%JAVA_HOME%\jre\lib\security\cacerts"

导入证书到jdk,输入确认命令的时候,是y不是yes

方式二:

keytool -import -alias casserver -file E:\Cas-SSO\ssl\casserver.cer -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -storepass changeit -trustcacerts

【说明】:-file指定证书的位置,也就是上一步导出证书的位置,命令中指定了JAVA_HOME,意思是将证书导入到客户端证书库,也就是jdk证书库中.因为客户端应用运行在本地,需要jdk的支持。回车之后,会让你输入密钥库口令,注意,这里的密码必须要输入changeit(jdk密码),不能输入上面指定的密码123456,切记,否则导入客户端证书会有问题,如果是多台机器演示,需要在每一台客户端导入该证书,步骤都是一样的。当看到提示"是否信任此证书",输入y回车即可,见下图:(说明,命令中的-alias后面的别名可以自定义,如果出现【证书未导入,别名<***>已经存在】的错误,该意思是说客户端的密钥库中已经存在该别名证书了,重新指定其他别名即可.)

证书查看

#查看证书信息

keytool -list -keystore E:\Cas-SSO\ssl\cas

#查看jdk目录下的证书

keytool -list -keystore "%JAVA_HOME%\jre\lib\security\cacerts" | findstr/i casserver

keytool -list -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -alias casserver

配置hosts

环境准备

· jdk1.8

· apache-tomcat-8.5.15

· cas-client-core-3.4.0.jar、commons-logging.jar

· 确保本地jdk环境已经搭建好

通用客户端配置

客户端地址:http://client.cas.com:8080/clientcas服务地址:https://server.cas.com:8443/cas

jar包导入

方式一:

在pom.xml配置中,需要导入cas的客户端,也可自行下载依赖包添加进去。

<!-- cas client -->

<dependency>

<groupId>org.jasig.cas.client</groupId>

<artifactId>cas-client-core</artifactId>

<version>3.4.1</version>

<exclusions>

<exclusion>

<groupId>javax.servlet</groupId>

<artifactId>servlet-api</artifactId>

</exclusion>

</exclusions>

</dependency>

方式二:

需要将提供的cas-client-core-3.2.1.jar和commons-logging.jar放置到程序所在的lib下,并配置jar包路径为应用的classpath中。如:WebRoot\WEB-INF\lib中。

创建资源过滤器类

如果我们需要某些系统资源不需要单点登录校验,我们可以在web.xml中直接设置需要过滤路径的正则表达式,过滤掉这些资源的登录校验,也可以自定义UrlPatternMatcherStrategy接口的实现类。

创建一个过滤器类SimpleUrlPatternMatcherStrategy,这个类需要实现UrlPatternMatcherStrategy接口,添加 matches里面的方法,来判断是否过滤掉这条请求。

import org.jasig.cas.client.authentication.UrlPatternMatcherStrategy;

/**

* 机能概要:过滤掉一些不需要授权,登录的界面

*/

public class SimpleUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy {

/**

* 机能概要: 判断是否匹配这个字符串

* @param url 用户请求的连接

* @return true : 我就不拦截你了

* false :必须得登录了

*/

@Override

public boolean matches(String url) {

//当含有loginout的字段,就可以不用登录了

return url.contains("/loginOut/success");

}

/**

* 正则表达式的规则,这个地方可以是web传递过来的

*/

@Override

public void setPattern(String pattern) {

}

}

配置web.xml

接下来是客户端配置的主要内容,主要是为了让CAS客户端与CAS服务端建立连接,需要在web.xml配置相关过滤器 (加色部分需要修改为实际参数)。

其中SingleSignOutFilter必须是首个过滤器。

<!--<!-- CAS Begin -->

<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->

<listener>

<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>

</listener>,

<!-- 该过滤器用于实现单点登出功能,可选配置 -->

<filter>

<filter-name>CAS Single Sign Out Filter</filter-name>

<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>CAS Single Sign Out Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!—单点登录验证过滤器 -->

<filter>

<filter-name>CAS Authentication Filter</filter-name>

<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>

<init-param>

<param-name>casServerLoginUrl</param-name>

<!--这里的server是CAS服务端的登录地址,login为固定值-->

<param-value>https://server.cas.com:8443/cas/login</param-value>

</init-param>

<init-param>

<param-name>serverName</param-name>

<!--这里是应用地址,注意是域名:端口或者ip:端口-->

<param-value>http://client.cas.com:8080/client</param-value>

</init-param>

<!-- 不需要匹配的类,可不要 -->

<init-param>

<param-name>ignoreUrlPatternType</param-name>

<param-value>com.yellowcong.auth.SimpleUrlPatternMatcherStrategy</param-value>

</init-param>

<!-- 不需要认证地址的正则pattern,可不要 -->

<init-param>

<param-name>ignorePattern</param-name>

<param-value>.*</param-value>

</init-param>

</filter>

<filter-mapping>

<filter-name>CAS Authentication Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->

<filter>

<filter-name>CAS Validation Filter</filter-name>

<filter-class>

org.jasig.cas.client.validation. Cas30ProxyReceivingTicketValidationFilter</filter-class>

<init-param>

<param-name>casServerUrlPrefix</param-name>

<!--这里的server是CAS服务端的地址,这里不要加login-->

<param-value>https://server.sodb.com:8443/cas</param-value>

</init-param>

<init-param>

<param-name>serverName</param-name>

<!--这里是应用地址,注意是域名:端口或者ip:端口-->

<param-value>http://client.cas.com:8080/client</param-value>

</init-param>

<init-param>

<param-name>redirectAfterValidation</param-name>

<param-value>true</param-value>

</init-param>

<init-param>

<param-name>useSession</param-name>

<param-value>true</param-value>

</init-param>

</filter>

<filter-mapping>

<filter-name>CAS Validation Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!--

该过滤器负责实现HttpServletRequest请求的包裹,比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。

-->

<filter>

<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>

<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!--

该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。比如AssertionHolder.getAssertion().getPrincipal().getName()。

-->

<filter>

<filter-name>CAS Assertion Thread Local Filter</filter-name>

<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>CAS Assertion Thread Local Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!-- CAS end -->

以上的核心点有两个:

· serverName 客户端访问路径

· casServerUrl cas服务路径

但上面的配置非常的简单,除了被排除的所有路径都会跳转进行登录

再加一个需求,判断路径是否需要跳转到登录页再跳转,那么不得不介绍一下AuthenticationFilter的一些简单配置:

配置单点登出

退出的时候,有两种方式,一种是走默认的退出页面,另一种是,退出后,跳转到哪一个界面,当然,这个界面必须能允许未登录用户访问。

1. 直接登出,返回CAS默认登出视图:

https://server.cas.com:8443/logout

2. 系统登出,并返回CAS指定返回页面 (参数名为service)

https://server.cas.com:8443/logout?service=xxx

控制器类:

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

/**

*功能说明:用户控制器

*/

@RequestMapping("/user")

@Controller

public class UserController {

/**

* 机能概要:单点登出

* @param session

* @return

*/

@RequestMapping("/loginOut")

public String loginOut(HttpSession session){

session.invalidate();

//http://yellowcong.com:8080/cas-client-maven/user/loginOut/success

//这个是直接退出,走的是默认退出方式

return "redirect:https://server.cas.com:8443/logout";

}

@RequestMapping("/loginOut2")

public String loginOut2(HttpSession session){

session.invalidate();

//退出登录后,跳转到退成成功的页面,不走默认页面

return "redirect:https://server.cas.com:8443/logout?service=http://client.cas.com:8080/client/user/loginOut/success";

}

/**

* 功能概要:退出成功的界面

* @return

*/

@RequestMapping("/loginOut/success")

public String loginOutPage(){

return "user/loginOut";

}

}

访问

成功启动客户端服务后,访问应用会被重定向到CAS统一认证界面,界面如下

输入用户名和密码后会跳转至各自应用界面。

获取用户

String userName = request.getRemoteUser(); // 获取用户名信息

// 方式1: 通过request对象获取

Principal principal = request.getUserPrincipal();

Map<String, Object> map = null;

if(principal!=null && principal instanceof AttributePrincipal){

AttributePrincipal aPrincipal = (AttributePrincipal)principal;

//获取用户信息中公开的Attributes部分

map = aPrincipal.getAttributes();

// 获取姓名,可以根据属性名称获取其他属性

}

// 方式2:通过session获取

Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);

Map<String, Object> map = assertion.getAttributes();

// 方式3:通过AssertionHolder类获取

// 获取用户名

String name = AssertionHolder.getAssertion().getPrincipal().getName();

// 获取用户属性

Map<String, Object> map = AssertionHolder.getAssertion().getPrincipal().getAttributes();

自定义客户端集成

上面说到了基本的单点登录客户端集成,但是如果想要在实现单点登录的同时,保留原有的登录功能(当单点登录服务有问题的时候)。这就需要我们在自己系统的登录校验机制中集成单点的;同时如果存在自己系统的登录标识,需要适配单点的登录。

实现思路:

1. 在单点中过滤掉本系统的所有关于登录相关的链接地址,不进行单点拦截

2. 在登录模块方法中,登录成功后模拟一个单点登录信息(Assertion对象)并放入session中

3. 在client系统的登录校验中加入单点的登录校验(比如:拦截器的方式实现)

4. 获取用户信息的方式加入从单点获取的方式

5. 系统登出的时候加上单点登出

查看AuthenticationFilter代码可知,过滤器中通过从session中获取Assertion对象来判断是否需要做登录验证,如果想要自己登录的时候不走单点校验了(骗过单点以为已登录),需要我们在走AuthenticationFilter前自己模拟一个Assertion对象并放入session中即可(或者我们重写AuthenticationFilter类的doFilter方法加入加入自己的登录判断)。

将自己的信息适配到单点登录中有两种实现思路:

1. 在登录成功后, 将登录信息生成一个Assertion对象放入Session中。

2. 在AuthenticationFilter 过滤器前添加一个过滤器,先做session判断是否存在Assertion对象,如果不存在则 根据登录信息生成一个Assertion对象并放入session中。

为此封装了一个工具类CASUtil。该类封装了对登录信息转换Assertion存入session、单点信息获取、判断是否是子系统登录放入session的判断等方法。

说明:

模拟单点登录信息

在登录接口中,登录成功后调用CASUtil.setUserToCasIfNotLogin(HttpServletRequest request, String principal, Map<String, Object> attributes)方法,将登录数据生成Assertion对象并存入session中。

如下:

Client登录校验集成单点

在登录校验中,如果本地登录校验不通过,然后判断是否存在CAS登录标识。如果存在则认为成功并放行请求,否则认证失败。

如下方法判断是否存在单点登录标识:

CASUtil.hasCASUser(request);

本系统是在拦截器中实现:

集成用户信息获取

获取用户信息,可以先从CAS中获取用户信息(ID等),然后可以根据自己系统业务获取用户信息。

获取单点用户信息:

Map<String, Object> assertion = CASUtil.getCASUser(request);// 获取单点登录用户信息

如下:

集成登出功能

在登出接口中,调用CASUtil.isSelfLogin(request) 方法判断是否是client系统登录,如果单点登录,则重定向调用CAS的登出地址,并带上service参数指向项目地址。

如下:

双向认证

有时候或许单点登录的时效 和 系统自身的登录时效不统一,导致单点 或者 系统自身验证单一方面失效,我们可以在单点过滤器前 增加一个过滤器,进行校验判断。

参考

github 项目地址:

https://github.com/apereo

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码