Spring Boot+Spring Security+CAS实现单点登录

Spring Boot+Spring Security+CAS实现单点登录,第1张

概述目录第一章 CAS的概述1.1、SSO1.2、CAS第二章 CAS的流程2.1、CAS服务端2.2、CAS客户端2.3、CAS流程图第三章 CAS的部署3.1、源码下载3.2、源码打包3.3、部署运行第四章 CAS的定制4.1、定制数据源4.2、兼容 HTTP4.3、定制登录页第五章 CAS的集成5.1、工程创建5.2、导入依赖5.3、修改包名5.4、编写配置文件5.5、编写角色授权5.6、编写配置对象5.7、编写控制器类5.8、启动项目测试配套资料,免费下载链接:https://pan.baidu.

目录 第一章 CAS的概述1.1、SSO1.2、CAS第二章 CAS的流程2.1、CAS服务端2.2、CAS客户端2.3、CAS流程图第三章 CAS的部署3.1、源码下载3.2、源码打包3.3、部署运行第四章 CAS的定制4.1、定制数据源4.2、兼容 HTTP4.3、定制登录页第五章 CAS的集成5.1、工程创建5.2、导入依赖5.3、修改包名5.4、编写配置文件5.5、编写角色授权5.6、编写配置对象5.7、编写控制器类5.8、启动项目测试

配套资料,免费下载
链接:https://pan.baIDu.com/s/1EINPwP4or0Nuj8BOEPsIyw
提取码:kbue
复制这段内容后打开百度网盘手机App, *** 作更方便哦

第一章 CAS的概述 1.1、SSO

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的一种分布式登录方式。

1.2、CAS

CAS(Central Authentication Service的缩写,中央认证服务)是耶鲁大学 Technology and Planning 实验室的 Shawn Bayern 在2002年出的一个开源系统。刚开始名字叫Yale CAS。Yale CAS 1.0的目标只是一个单点登录的系统,随着慢慢公开,功能就越来越多了,2.0就提供了多种认证的方式。从结构上看,CAS 包含两个部分: CAS Server 和 CAS ClIEnt。

2004年12月,CAS转成 JASIG(Java administration Special Interesting Group) 的一个项目,项目也随着改名为 JASIG CAS,这就是为什么现在有些CAS的链接还是有 jasig 的字样。

2012年,JASIG 跟 Sakai 基金会合并,改名为 Apereo 基金会,所有 CAS 也随着改名为 Apereo CAS。

官网地址:https://www.apereo.org/projects/cas

源码地址:https://github.com/apereo/cas-overlay-template/tree/5.3

CAS 具有以下特点:

开源的企业级单点登录解决方案。CAS Server 为需要独立部署的 Web 应用。CAS ClIEnt 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包 括 Java,.Net,PHP,Perl,Apache,uPortal,Ruby 等。第二章 CAS的流程 2.1、CAS服务端

CAS Server 需要独立部署,主要负责对用户的认证工作;

2.2、CAS客户端

CAS ClIEnt 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。

2.3、CAS流程图

下图是 CAS 最基本的协议过程,主要有以下步骤:

访问服务: CAS 客户端发送请求访问应用系统提供的服务资源。定向认证: CAS 客户端会重定向用户请求到 CAS 服务器。用户认证:用户身份认证。发放票据: CAS 服务器会产生一个随机的 Service Ticket 。验证票据: CAS 服务器验证票据 Service Ticket 的合法性,验证通过后,允许客户端访问服务。传输用户信息:CAS 服务器验证票据通过后,传输用户认证结果信息给客户端。

第三章 CAS的部署 3.1、源码下载

下载地址:https://codeload.github.com/apereo/cas-overlay-template/zip/5.3

注意:由于github是在国外,可能个别人下载会很慢,你可以使用手机下载试试,并且尝试一下不同的网络。

3.2、源码打包

打包命令:mvn clean package -Dmaven.test.skip=true

注意:打包的速度是很快的,但是打包所需要的依赖均在互联网上,个别包非常大,第一次打包会下载很多东西很慢,请耐心等候,如果失败请多尝试几次。

更换下载源

如果你想要下载速度快点,请更换下载源,找到pom.xml中的repositorIEs,把以下代码放到第一个,具体如下图:

<repository>    <ID>aliyunmaven</ID>    <url>http://maven.aliyun.com/nexus/content/groups/public/</url></repository>

3.3、部署运行

由于打完包后是一个war包,需要tomcat进行部署,然后才能运行,如果你没有tomcat请下载。

32位tomcat:https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-8/v8.5.61/bin/apache-tomcat-8.5.61-windows-x86.zip64位tomcat:https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-8/v8.5.61/bin/apache-tomcat-8.5.61-windows-x64.zip

将打包好的war包,放到tomcatwebapps目录中,然后点击bin目录下的startup.bat启动。

注意:如果你在启动的过程中,发现tomcat一闪而过或启动失败,可能的原因就是jdk的环境变量没有配好,在一个就是端口占用了,tomcat默认端口为8080。

如果一切运行正常,那么你在浏览器中输入地址:http://localhost:8080/cas/login,将会看到如下界面:

默认用户名:casuser

默认密码:Mellon

@H_528_301@

除了使用上边提供的登出链接,还可以手动在地址栏输入http://localhost:8080/cas/logout

第四章 CAS的定制 4.1、定制数据源

(1)、加入数据源依赖包

找到源码目录cas-overlay-template-5.3中的pom.xml,在第125-127注释后边加入如下依赖代码:

<!--数据库认证相关 start--><dependency>    <groupID>org.apereo.cas</groupID>    <artifactID>cas-server-support-jdbc</artifactID>    <version>${cas.version}</version></dependency><dependency>    <groupID>org.apereo.cas</groupID>    <artifactID>cas-server-support-jdbc-drivers</artifactID>    <version>${cas.version}</version></dependency><dependency>    <groupID>MysqL</groupID>    <artifactID>mysql-connector-java</artifactID>    <version>5.1.49</version></dependency><!--数据库认证相关 end-->

(2)、重新打包部署项目

重新打包

找到源码目录cas-overlay-template-5.3,在地址栏输入cmd,然后回车,运行打包命令:mvn clean package -Dmaven.test.skip=true,这一次打包速度会很快,因为大部分依赖都已经下载到了本地,但是因为又新增了部分数据源的依赖,所以还是会联网下载,请耐心等待,我发誓这是最后一次打包了。

关闭项目

把之前开启的tomcat关掉。

删除文件

找到apache-tomcat-8.5.61\webapps目录,把里边所有的文件及目录全部删除干净(包括tomcat自带的),因为都没有用,留着反而会影响启动速度。

重新部署

把打包好的war包,重新放到webapps目录中,然后启动tomcat服务器进行解压。

(3)、加入数据源的配置

打开apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes解压后的类路径目录,在这里你将会看到很多配置文件,接下来,我们需要加上数据库的相关配置,具体代码如下,把他复制到application.propertIEs的最后部分,将 cas.authn.accept.users=.... 加井号注释掉,再加入以下内容:

cas.authn.jdbc.query[0].driverClass=com.MysqL.jdbc.Drivercas.authn.jdbc.query[0].url=jdbc:MysqL://localhost:3306/testcas.authn.jdbc.query[0].user=rootcas.authn.jdbc.query[0].password=rootcas.authn.jdbc.query[0].sql=select password from sys_user where username=?cas.authn.jdbc.query[0].fIEldPassword=password

(4)、加入密码加密规则

cas5.X 提供了4种加密配置:

cas.authn.jdbc.query[0].passwordEncoder.type=NONE|DEFAulT|STANDARD|BCRYPT

默认值为 NONE这四种方式其实脱胎于spring security中的加密方式,spring security提供了 MD5PasswordEncoder、SHAPasswordEncoder、StandardPasswordEncoder和 BCryptPasswordEncoder。

NONE:说明对密码不做任何加密,也就是保留明文。

DEFAulT:启用DefaultPasswordEncoder。 MD5PasswordEncoder和 SHAPasswordEncoder加密是编码算法加密,现在cas把他们归属于 DefaultPasswordEncoder。DefaultPasswordEncoder需要带参数 enCodingAlgorithm:

cas.authn.accept.passwordEncoder.enCodingAlgorithm=MD5|SHA

STANDARD:启用StandardPasswordEncoder加密方式 。1024次迭代的 SHA‐256 散列 哈希加密实现,并使用一个随机8字节的salt。

BCRYPT:启用BCryptPasswordEncoder加密方式。

我们这里采用BCRYPT加密,因为我们数据库中的密码加密方式就是这种加密,应该保持一致,修改application.propertIEs 配置文件。

cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPTcas.authn.jdbc.query[0].passwordEncoder.characterEnCoding=UTF-8

(5)、导入数据库数据表

DROP DATABASE IF EXISTS `test`;CREATE DATABASE `test`;USE `test`;DROP table IF EXISTS `sys_role`;CREATE table `sys_role` (  `ID` int(11) NOT NulL auto_INCREMENT COMMENT '角色编号',  `name` varchar(32) NOT NulL COMMENT '角色名称',  `desc` varchar(32) NOT NulL COMMENT '角色描述',  PRIMARY KEY (`ID`)) ENGINE=InnoDB auto_INCREMENT=5 DEFAulT CHARSET=utf8;insert  into `sys_role`(`ID`,`name`,`desc`) values (1,'RolE_USER','用户权限');insert  into `sys_role`(`ID`,`desc`) values (2,'RolE_admin','管理权限');insert  into `sys_role`(`ID`,`desc`) values (3,'RolE_PRODUCT','产品权限');insert  into `sys_role`(`ID`,`desc`) values (4,'RolE_ORDER','订单权限');DROP table IF EXISTS `sys_user`;CREATE table `sys_user` (  `ID` int(11) NOT NulL auto_INCREMENT COMMENT '用户编号',  `username` varchar(32) NOT NulL COMMENT '用户名称',  `password` varchar(128) NOT NulL COMMENT '用户密码',  `status` int(1) NOT NulL DEFAulT '1' COMMENT '用户状态(0:关闭、1:开启)',  PRIMARY KEY (`ID`)) ENGINE=InnoDB auto_INCREMENT=4 DEFAulT CHARSET=utf8;insert  into `sys_user`(`ID`,`username`,`password`,`status`) values (1,'zhangsan','a$M7fmKpMZEkkzrTBiKIE.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',0);insert  into `sys_user`(`ID`,`status`) values (2,'lisi',`status`) values (3,'wangwu',0);DROP table IF EXISTS `sys_user_role`;CREATE table `sys_user_role` (  `uID` int(11) NOT NulL COMMENT '用户编号',  `rID` int(11) NOT NulL COMMENT '角色编号',  PRIMARY KEY (`uID`,`rID`)) ENGINE=InnoDB DEFAulT CHARSET=utf8;insert  into `sys_user_role`(`uID`,`rID`) values (1,1);insert  into `sys_user_role`(`uID`,3);insert  into `sys_user_role`(`uID`,`rID`) values (2,4);insert  into `sys_user_role`(`uID`,`rID`) values (3,2);insert  into `sys_user_role`(`uID`,4);

(6)、重新启动tomcat

把之前启动的tomcat关闭,然后重新启动,让咱们的配置生效。

(7)、重新开浏览器测试

打开浏览器输入地址:http://localhost:8080/cas/login

账户:zhangsan密码:全部都是1234564.2、兼容 http

(1)、由于CAS默认使用的是基于https协议,需要改为兼容使用http协议,到apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes目录的application.propertIEs添加如下的内容,其中,TGC:Ticket Granted cookie (客户端用户持有,传送到服务器,用于验证)存放用户身份认证凭证的cookie,在浏览器和CAS Server间通讯时使用,并且只能基于安全通道传输(https),是CAS Server用来明确用户身份的凭证。

cas.tgc.secure=falsecas.serviceRegistry.initFromJson=true

(2)、到apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes\services目录下的 httpSandIMAPS-10000001.Json 修改内容如下,即添加http

{  "@class" : "org.apereo.cas.services.RegexRegisteredService","serviceID" : "^(https|http|imaps)://.*","name" : "httpS and IMAPS","ID" : 10000001,"description" : "This service deFinition authorizes all application urls that support httpS and IMAPS protocols.","evaluationorder" : 10000}

(3)、重新启动tomcat,然后登录zhangsan进行测试,或许看起来并没有什么不一样,但是现在已经兼容http协议了。

4.3、定制登录页

相关规范

● 静态资源(Js、CSS)存放目录为WEB-INF\classes\static
● HTML资源(thymeleaf模板)存放目录为WEB-INF\classes\templates
● 主题配置文件存放在WEB-INF\classes,并且命名为[theme_name].propertIEs

准备工作

注意:配套资料中有静态资源,请在里边找到cas单点登录基础代码\登录页面源代码

(1)、在静态资源目录(WEB-INF\classes\static\themes)下创建一个文件夹,一般跟工程名字保持一致,这里我们叫mylogin,然后把咱们的CSS和Js都拷贝进去。

(2)、在模板资源目录(WEB-INF\classes\static\templates)下创建一个文件夹,一般跟工程名字保持一致,这里我们叫mylogin,把咱们的静态资源 login.HTML 拷贝到这里,然后把名称改为 casLoginVIEw.HTML

(3)、给登录页面模板添加thymeleaf的命名空间

<!DOCTYPE HTML><HTML xmlns:th="http://www.thymeleaf.org">

(4)、修改所有静态资源的路径,改成绝对路径

<!DOCTYPE HTML><HTML xmlns:th="http://www.thymeleaf.org"><head>    <Title>自定义登录页</Title>    <link rel="stylesheet" href="/cas/themes/mylogin/CSS/bootstrap.min.CSS"></head><body>......    <script src="/cas/themes/mylogin/Js/jquery-3.5.1.min.Js"></script><script src="/cas/themes/mylogin/Js/bootstrap.bundle.min.Js"></script></body></HTML>

(5)、修改表单的提交路径并加上特定对象属性

<form th:object="${credential}" action="login" method="post">......    </form>

(6)、修改表单的用户名和密码文本框都添加上一个th:fIEld属性,并去掉无用属性

...<input type="text" class="form-control" th:fIEld="*{username}" placeholder="请输入用户" required>...<input type="text" class="form-control" th:fIEld="*{password}" placeholder="请输入密码" required>...

(7)、为此表单添加登录失败错误信息以及隐藏域表单项,直接拷贝以下代码到button提交按钮上边即可

<div class="form-group" th:if="${#fIElds.hasErrors('*')}">    <span th:each="err : ${#fIElds.errors('*')}" th:utext="${err}"></span></div><input type="hIDden" name="execution" th:value="${flowExecutionKey}"/><input type="hIDden" name="_eventID" value="submit"/><input type="hIDden" name="geolocation"/>

(8)、在apache-tomcat-8.5.61\webapps\cas\WEB-INF\classes目录下创建一个主题配置文件,名字叫mylogin.propertIEs,配置内容如下:

cas.standard.CSS.file=/CSS/cas.CSS

(9)、找到application.propertIEs配置文件,在文件的最后,我们启用默认主题为我们自己的主题即可。

cas.theme.defaultthemename=mylogin

(10)、重启tomcat应用,访问登录地址:http://localhost:8080/cas/login,如果一切顺利,那么,您将会看到如下界面:

(11)、登录测试,使用账号zhangsan,密码:123456进行登录测试。好就这样,我们保持工程启动状态,接下来,进行下一环节。

第五章 CAS的集成 5.1、工程创建

@[email protected]、导入依赖

pom.xml

<!--跟数据库进行的整合--><dependency>    <groupID>org.mybatis.spring.boot</groupID>    <artifactID>mybatis-spring-boot-starter</artifactID>    <version>2.1.4</version></dependency><dependency>    <groupID>MysqL</groupID>    <artifactID>mysql-connector-java</artifactID>    <version>5.1.49</version></dependency><!--跟Spring Security+CAS进行的整合--><dependency>    <groupID>org.springframework.boot</groupID>    <artifactID>spring-boot-starter-security</artifactID></dependency><dependency>    <groupID>org.springframework.security</groupID>    <artifactID>spring-security-cas</artifactID></dependency>
5.3、修改包名

com.caochenlei.casresourceorder修改为com.caochenlei,然后在分别创建以下包名

com.caochenlei.domain:用于存放实体对象com.caochenlei.prop:用于存放配置属性类com.caochenlei.mapper:用于存放映射文件com.caochenlei.service:用于存放服务接口com.caochenlei.controller:用于存放控制器类com.caochenlei.config:用于存放配置对象

将资料中提供的 cas单点登录基础代码\映射服务的代码 中的mapper和service中的代码直接拷贝到工程所对应的包中,也可以直接带包拷贝

5.4、编写配置文件

application.yaml

server:  port: 9001  servlet:    application-display-name: cas-resource-orderspring:  datasource:    driver-class-name: com.MysqL.jdbc.Driver    url: jdbc:MysqL://localhost:3306/test    username: root    password: rootmybatis:  type-aliases-package: com.caochenlei.domain  configuration:    map-underscore-to-camel-case: truelogging:  level:    com.caochenlei: deBUG#自定义配置:APP配置信息(资源端)app:  server:    host:      #APP服务地址(绝对路径)      url: http://localhost:9001      #APP登录地址(相对路径)      login_url: /login      #APP退出地址(相对路径)      logout_url: /logout#自定义配置:CAS配置信息(认证端)cas:  server:    host:      #CAS服务地址(绝对路径)      url: http://localhost:8080/cas      #CAS登录地址(绝对路径)      login_url: ${cas.server.host.url}/login      #CAS退出地址(绝对路径)      logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
5.5、编写角色授权

com.caochenlei.service.CustomUserDetailsService

@Service@Transactionalpublic class CustomUserDetailsService implements UserDetailsService {    @autowired(required = false)    private SysUserMapper sysUserMapper;    @OverrIDe    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        return sysUserMapper.findByUsername(username);    }}
5.6、编写配置对象

com.caochenlei.prop.CasPropertIEs

/** * 自定义属性配置类 */@Component@Datapublic class CasPropertIEs {    @Value("${cas.server.host.url}")    private String casServerUrl;    @Value("${cas.server.host.login_url}")    private String casServerLoginUrl;    @Value("${cas.server.host.logout_url}")    private String casServerlogoutUrl;    @Value("${app.server.host.url}")    private String appServerUrl;    @Value("${app.server.host.login_url}")    private String appServerLoginUrl;    @Value("${app.server.host.logout_url}")    private String appServerlogoutUrl;}

com.caochenlei.config.SecurityConfig

@Configuration@EnableWebSecurity //启用web权限@EnableGlobalMethodSecurity(securedEnabled = true) //启用方法验证public class SecurityConfig extends WebSecurityConfigurerAdapter {    @autowired    private CasPropertIEs casPropertIEs;    @OverrIDe    protected voID configure(AuthenticationManagerBuilder auth) throws Exception {        super.configure(auth);        auth.authenticationProvIDer(casAuthenticationProvIDer());    }    @OverrIDe    protected voID configure(httpSecurity http) throws Exception {        //禁用csrf保护机制        http.csrf().disable();        //禁用cors保护机制        http.cors().disable();        //禁用form表单登录        http.formLogin().disable();        //增加自定义过滤器        http.exceptionHandling()                .authenticationEntryPoint(casAuthenticationEntryPoint())                .and()                .addFilterat(casAuthenticationFilter(), CasAuthenticationFilter.class)                .addFilterBefore(logoutFilter(), logoutFilter.class)                .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);    }    /**     * CAS认证入口点开始 =============================================================================     */    @Bean    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();        casAuthenticationEntryPoint.setLoginUrl(casPropertIEs.getCasServerLoginUrl());        casAuthenticationEntryPoint.setServicePropertIEs(servicePropertIEs());        return casAuthenticationEntryPoint;    }    @Bean    public ServicePropertIEs servicePropertIEs() {        ServicePropertIEs servicePropertIEs = new ServicePropertIEs();        servicePropertIEs.setService(casPropertIEs.getAppServerUrl() + casPropertIEs.getAppServerLoginUrl());        servicePropertIEs.setAuthenticateallArtifacts(true);        return servicePropertIEs;    }    /**     * CAS认证入口点结束 =============================================================================     */    /**     * CAS认证过滤器开始 =============================================================================     */    @Bean    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();        casAuthenticationFilter.setAuthenticationManager(authenticationManager());        casAuthenticationFilter.setFilterProcessesUrl(casPropertIEs.getAppServerLoginUrl());        return casAuthenticationFilter;    }    @Bean    public CasAuthenticationProvIDer casAuthenticationProvIDer() {        CasAuthenticationProvIDer casAuthenticationProvIDer = new CasAuthenticationProvIDer();        casAuthenticationProvIDer.setAuthenticationUserDetailsService(userDetailsBynameServiceWrapper());        casAuthenticationProvIDer.setServicePropertIEs(servicePropertIEs());        casAuthenticationProvIDer.setTicketValIDator(new Cas20ServiceTicketValIDator(casPropertIEs.getCasServerUrl()));        casAuthenticationProvIDer.setKey("an_ID_for_this_auth_provIDer_only");        return casAuthenticationProvIDer;    }    @Bean    public UserDetailsBynameServiceWrapper userDetailsBynameServiceWrapper() {        UserDetailsBynameServiceWrapper userDetailsBynameServiceWrapper = new UserDetailsBynameServiceWrapper();        userDetailsBynameServiceWrapper.setUserDetailsService(userDetailsService());        return userDetailsBynameServiceWrapper;    }    @Bean    public UserDetailsService userDetailsService() {        return new CustomUserDetailsService();    }    /**     * CAS认证过滤器结束 =============================================================================     */    /**     * CAS登出过滤器开始 =============================================================================     */    @Bean    public logoutFilter logoutFilter() {        logoutFilter logoutFilter = new logoutFilter(casPropertIEs.getCasServerlogoutUrl(), new SecurityContextlogoutHandler());        logoutFilter.setFilterProcessesUrl(casPropertIEs.getAppServerlogoutUrl());        return logoutFilter;    }    @Bean    public SingleSignOutFilter singleSignOutFilter() {        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();        singleSignOutFilter.setIgnoreInitConfiguration(true);        return singleSignOutFilter;    }    /**     * CAS登出过滤器结束 =============================================================================     */}
5.7、编写控制器类

com.caochenlei.controller.OrderController

@RestController@RequestMapPing("/order")public class OrderController {    @Secured({"RolE_admin", "RolE_ORDER"})    @RequestMapPing("/info")    public String info() {        return "Order Controller ...";    }}
5.8、启动项目测试

启动项目,在浏览器输入地址进行访问:http://localhost:9001/order/info ,发现报错了,报错的原因很简单,zhangsan不能访问订单,只有lisi可以。

那我们使用退出链接 http://localhost:9001/logout 进行退出。

然后使用lisi进行重新访问:http://localhost:9001/order/info。

登录以后,我们就可以看到我们的业务逻辑执行后所返回的数据了。

总结

以上是内存溢出为你收集整理的Spring Boot+Spring Security+CAS实现单点登录全部内容,希望文章能够帮你解决Spring Boot+Spring Security+CAS实现单点登录所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1210451.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-04
下一篇 2022-06-04

发表评论

登录后才能评论

评论列表(0条)

保存