单点登录(二) : 简单搭建CAS和测试认证

我们在上一篇文章中已经介绍了cas以及它的工作流程。

本章我们开始动手搭建一个完整的cas服务实现2个web之间的单点登录。

简化声明

我们这里为了感受完整的简单的CAS搭建流程,这里有两个地方做了简化:

认证方式使用的测试类型的认证方式

我们在上一篇已经学习了CAS的认证方式支持很多种,可以是xml,可以是LADP服务,可以是数据库,我们这里因为是测试,所以先使用最简单的认证方式,也就是cas原生提供的测试用的一种方式,帐号密码是写死的。

去除https的证书认证

我们在看网上很多CAS搭建的教程中会看到很多都有配置导入导入证书这个步骤,很繁琐而且容易失败或者出问题。

测试环境中自己用JDK自带的keytool工具生成证书。

如果以后真正在产品环境中使用肯定要去证书提供商去购买,证书认证一般都是由VeriSign认证,

中文官方网站: http://www.verisign.com/cn/。 

也可以申请免费的StartSSL CA证书: StartSSL(公司名:StartCom)也是一家CA机构,它的根证书很久之前就被一些具有开源背景的浏览器支持(Firefox浏览器、谷歌Chrome浏览器、苹果Safari浏览器等)。 

申请地址:http://www.startssl.com

 申请方法参考: http://www.linuxidc.com/Linux/2011-11/47478.htm 

而且不是官方付费购买的证书权限的话,浏览器还会报证书过期等等。

CAS默认使用的是HTTPS协议,如果对安全要求不高,可使用HTTP协议。

所以不一定需要配置证书,我们后面会讲到启用https时证书怎么配置。

但是这里先使用http协议,可以使我们的搭建流程更简便。

cas server版本选择

我们在cas的github项目中可以看到版本的发布情况,我们可以自己根据特性来选择版本。

https://github.com/apereo/cas/releases

点击DOCS然后查看Getting Started中的Installation Requirements可以看到需要哪些支持。

原则上越新的稳定版本越好,但是看到5.0版本以上基本都需要jdk1.8以上的支持,而且需要gradle构建工具来进行编译,4.1.11则需要maven3.3版本进行编译。

因为新版本的源码部署打包稍微麻烦一些,我们独立出来讲解在

单点登录(三)-----实战-----cas server 源码下载和部署

官网中4.0.0版本是有发布直接可用的war包版本的,我们直接使用4.0.0作为例子。

所以我选择了 CAS v4.0.0版本。

https://github.com/apereo/cas/releases

在下载页面找到4.0.0后选择 release.zip下载(只有4.0.0有这个选项,其他版本都只有源码):

 

cas server解压war包部署到tomcat

war包名修改

我们把下载好的release.zip解压出来得到cas server 4.0.0的文件夹,文件夹中有module文件夹里面有可用的war包。

注意只有4.0.0版本的有module文件夹和可用war包,其他版本的源码zip解压出来是没有的,需要自己编译打包。

 

ps:!!!!注意是cas-server-webapp-4.0.0.war包,别用错cas-management-webapp-4.0.0.war了,否则后面会出现页面重定向错误http://localhost:9000/cas/login?service=http%3A%2F%2Flocalhost%3A9000%2Fcas-management%2Fj_spring_cas_security_check。

然后我们把war包复制到tomcat的webapp文件夹,为了方便访问,我们先把war包名称修改成cas.war。运行tomcat即可解压出cas-server项目。

这样我们就能访问cas-server项目了。

端口修改

如果tomcat的端口不是用的8080的话 还需要在解压出来的cas工程中修改一下配置文件cas.properties中的server.name,如图:

我这里把它修改成http://localhost:9000

然后重启tomcat

然后访问的时候就能使用

http://localhost:9000/cas/login

访问。

这里的9000端口是我的tomcat设置的端口,读者自己对应自己的端口。

 

然后我们尝试使用http://localhost:9000/cas/login访问。

出现如下页面则部署成功:

 

但是我们注意到这里有一句提醒:

You are currently accessing CAS over a non-secure connection. Single Sign On WILL NOT WORK. In order to have single sign on work, you MUST log in over HTTPS.

意思是我们不是通过https协议连接的。

去掉https验证

cas默认是采用https模式的,我们没有配置证书,所以要么配置证书,要么取消https的过滤,让http协议也能访问。

我们这里取消https的配置,让http也能访问。

需要修改三个地方的配置(针对4.0.0版本,其他版本的话第一处必改,其他的看看还有没有带有cookie的文件名)

修改一deployerConfigContext.xml增加参数p:requireSecure="false"

我们在tomcat的webapp中找到 cas/WEB-INF/deployerConfigContext.xml

里面有一句

<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient"/>

这里需要增加参数p:requireSecure="false",requireSecure属性意思为是否需要安全验证,即HTTPS,false为不采用。修改后为:

  <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
 p:httpClient-ref="httpClient"  p:requireSecure="false"/>

修改二ticketGrantingTicketCookieGenerator.xml修改p:cookieSecure="false"

 我们在tomcat的webapp中找到cas/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"

      p:cookieSecure="true"

      p:cookieMaxAge="-1"

      p:cookieName="CASTGC"

      p:cookiePath="/cas" />

 参数p:cookieSecure="true",同理为HTTPS验证相关,TRUE为采用HTTPS验证,FALSE为不采用https验证。

参数p:cookieMaxAge="-1",简单说是COOKIE的最大生命周期,-1为无生命周期,即只在当前打开的IE窗口有效,IE关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于0的数字,比如3600等,意思是在3600秒内,打开任意IE窗口,都不需要验证。

 这里把 cookieSecure修改为false就行了

修改三 warnCookieGenerator.xml修改p:cookieSecure="false"

我们在tomcat的webapp中找到cas/WEB-INF/spring-configuration/warnCookieGenerator.xml

 

里面有

<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="true"
p:cookieMaxAge="-1"
p:cookieName="CASPRIVACY"
p:cookiePath="/cas" />

 参数p:cookieSecure="true",同理为HTTPS验证相关,TRUE为采用HTTPS验证,FALSE为不采用https验证。

 这里把 cookieSecure修改为false就行了。

 

三个地方都修改好后我们就可以启动tomcat了,这时候https的方式已经取消,可以使用http的方式访问了。

ps:我们这里还没配置cas 客户端也就是我们的web应用,但是记得把客户端filter中的http链接也修改成http即可。  

例如:所有https://localhost:9000/cas   更改为 http://localhost:9000/cas

PS:配置完http方式访问之后页面上的 提示还是存在的,如果我们之后会对登录界面样式完全改版,所以可以不用管它。如果还是需要把它去掉的话,cas统一认证的登陆页面位于:cas目录/WEB-INF/view/jsp/default 文件夹里,其中ui/casLoginView.jsp为登陆页面。我们找到这段代码删掉即可。

<c:if test="${not pageContext.request.secure}">
  <div id="msg" class="errors">
    <h2>Non-secure Connection</h2>
    <p>You are currently accessing CAS over a non-secure connection.  Single Sign On WILL NOT WORK.  In order to have single sign on work, you MUST log in over HTTPS.</p>
  </div>
</c:if>

 

尝试登录

我们之前说过cas server对用户密码的认证是支持很多种方式的,我们可以进行配置。

4.0.0版本的cas是在deployerConfigContext.xml文件中进行配置的。我们找到这个部分。

    <!--
       | TODO: Replace this component with one suitable for your enviroment.
       |
       | This component provides authentication for the kind of credential used in your environment. In most cases
       | credential is a username/password pair that lives in a system of record like an LDAP directory.
       | The most common authentication handler beans:
       |
       | * org.jasig.cas.authentication.LdapAuthenticationHandler
       | * org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler
       | * org.jasig.cas.adaptors.x509.authentication.handler.support.X509CredentialsAuthenticationHandler
       | * org.jasig.cas.support.spnego.authentication.handler.support.JCIFSSpnegoAuthenticationHandler
       -->
    <bean id="primaryAuthenticationHandler"
          class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
        <property name="users">
            <map>
                <entry key="casuser" value="Mellon"/>
            </map>
        </property>
    </bean>


AcceptUsersAuthenticationHandler 是普通认证类型,也就是直接对照帐号密码。

可以看到测试版本的帐号是casuser密码是 Mellon,是写死的。我们就用这个帐号密码进行登录即可,或者修改成你喜欢的帐号密码再启动tomcat。

登录后显示登录成功说明我们的cas server部署成功了。

接着我们就要开始部署cas client了,也就是把我们的web项目于cas server关联起来。

 

cas client的部署和设置

我们在官网下载到的cas client项目不是web 项目的形式(不是war包),不能直接使用,它只是一个jar包,需要融合到我们的web项目,使web项目变得支持cas服务。

CAS client是部署在应用端的,因为通常单点登陆都会涉及到对已有系统的改造。所以,client端的侵入性就变的很重要。侵入性越小,越容易部署和测试。CAS框架的优点之一就在于它的client端对应用系统的侵入性比较小。对于Java的Web项目来说,你只需要在web.xml里面添加一个filter,拷贝CAS client的jar包到应用系统,然后改造登陆认证过程即可。如果CAS server用的是Https,那就还需要将证书导入到JVM的可信证书域中,通常是($JAVA_HOME/lib/security/cacerts)。

新建web项目

生成环境中是把已有的web项目进行改造,我们这里为了简化,可以新建一个web项目。

我在myeclipse中新建两个maven类型的web项目如下(分别命名为cas-client1和cas-client2):

 

cas-client-core.jar包引用

这里拷贝的cas client的jar包也就是官网提供的cas client项目。下载地址:

https://github.com/apereo/java-cas-client

下载到的是源代码,需要把cas-client-core打包成jar包才能使用。

将cas-client-core-3.2.1.jar放入应用WEB-INF/lib下即可。

如果我们的web项目是maven项目的话就方便很多,直接在pom.xml文件添加以下引用即可:

[java]  view plain  copy

  1. <dependency>  
  2.     <groupId>org.jasig.cas.client</groupId>  
  3.     <artifactId>cas-client-core</artifactId>  
  4.     <version>${java.cas.client.version}</version>  
  5. </dependency>  

${java.cas.client.version}替换成我们使用的client发布的版本号。

我们在tags中可以看到有哪些产品号

 

所以我们这里的会使用3.4.1版本。pom.xml中添加的代码为:

[java]  view plain  copy

  1. <dependency>  
  2.     <groupId>org.jasig.cas.client</groupId>  
  3.     <artifactId>cas-client-core</artifactId>  
  4.     <version>3.4.1</version>  
  5. </dependency>  

域名映射

正式生产环境中,cas client配置cas server的地址时如果是使用https验证的话不能使用ip的,只能用域名,域名需要与生成证书时填写的域名一样,如果是http模式验证的话,则cas server的地址可以使用ip。

我们这里是做测试,所以可以在cas client所在的系统上修改hosts文件做域名映射。

否则的话会报各种错误。

win 10的hosts文件在 C:\WINDOWS\system32\drivers\etc

编辑新增127.0.0.1 casserver 

保存

如果保存不了的话是权限问题,对着hosts文件右键属性安全里去设置即可。

验证一下  运行tomcat 我们的cas server能不能用域名访问,之前的访问地址是

http://localhost:9000/cas/login

现在尝试用

http://casserver:9000/cas/login 访问。

访问成功,域名映射就设置好了。

web.xml配置filter

包引入之后我们就可以使用cas的服务了,主要是要配置一下过滤器。

cas有几个重要的filter:
org.jasig.cas.client.authentication.AuthenticationFilter (负责客户端认证)
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter (按照CAS2体系结构校验Ticket)
org.jasig.cas.client.util.HttpServletRequestWrapperFilter (包装request.getRemoteUser()数据)
org.jasig.cas.client.util.AssertionHolder(来获取用户的登录名)

web.xml中需要增加的代码格式为(还需要根据我们的环境进行参数调整):

[java]  view plain  copy

  1. <!-- ****************** 单点登录开始 ********************-->  
  2.     <!-- 用于实现单点登出功能  可选 -->  
  3.     <listener>  
  4.         <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>  
  5.     </listener>  
  6.     <!-- 该过滤器用于实现单点登出功能,单点退出配置,一定要放在其他filter之前 可选 -->  
  7.     <filter>  
  8.         <filter-name>CAS Single Sign Out Filter</filter-name>  
  9.         <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>  
  10.         <init-param>  
  11.             <param-name>casServerUrlPrefix</param-name>  
  12.             <param-value>http://casserver:9000/cas/</param-value>  
  13.         </init-param>  
  14.     </filter>  
  15.     <filter-mapping>  
  16.         <filter-name>CAS Single Sign Out Filter</filter-name>  
  17.         <url-pattern>/*</url-pattern>  
  18.     </filter-mapping>  
  19.     <!-- 该过滤器负责用户的认证工作,必须 -->  
  20.     <filter>  
  21.         <filter-name>CASFilter</filter-name>  
  22.         <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>  
  23.         <init-param>  
  24.             <!--casServerLoginUrl:cas服务的登陆url -->  
  25.             <param-name>casServerLoginUrl</param-name>  
  26.             <param-value>http://casserver:9000/cas/login</param-value>  
  27.         </init-param>  
  28.         <init-param>  
  29.             <!--serverName:本项目的ip+port -->  
  30.             <param-name>serverName</param-name>  
  31.             <param-value>http://localhost:9000</param-value>  
  32.         </init-param>  
  33.         <init-param>  
  34.             <param-name>useSession</param-name>  
  35.             <param-value>true</param-value>  
  36.         </init-param>  
  37.         <init-param>  
  38.             <param-name>redirectAfterValidation</param-name>  
  39.             <param-value>true</param-value>  
  40.         </init-param>  
  41.     </filter>  
  42.     <filter-mapping>  
  43.         <filter-name><span style="font-family: Arial, Helvetica, sans-serif;">CASFilter</span>  
  44. </filter-name>  
  45.         <url-pattern>/*</url-pattern>  
  46.     </filter-mapping>  
  47.     <!-- 该过滤器负责对Ticket的校验工作,必须-->  
  48.     <filter>  
  49.         <filter-name>CAS Validation Filter</filter-name>  
  50.         <filter-class>  
  51.             org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter  
  52.         </filter-class>  
  53.         <init-param>  
  54.             <param-name>casServerUrlPrefix</param-name>  
  55.             <param-value>http://casserver:9000/cas/</param-value>  
  56.         </init-param>  
  57.         <init-param>  
  58.             <param-name>serverName</param-name>  
  59.             <param-value>http://localhost:9000</param-value>  
  60.         </init-param>  
  61.     </filter>  
  62.     <filter-mapping>  
  63.         <filter-name>CAS Validation Filter</filter-name>  
  64.         <!-- 对项目中的哪些路径做登录拦截-->  
  65.         <url-pattern>/*</url-pattern>  
  66.     </filter-mapping>  
  67.     <!-- 该过滤器对HttpServletRequest请求包装, 可通过HttpServletRequest的getRemoteUser()方法获得登录用户的登录名,可选 -->  
  68.     <filter>  
  69.         <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>  
  70.         <filter-class>  
  71.             org.jasig.cas.client.util.HttpServletRequestWrapperFilter  
  72.         </filter-class>  
  73.     </filter>  
  74.     <filter-mapping>  
  75.         <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>  
  76.         <url-pattern>/*</url-pattern>  
  77.     </filter-mapping>  
  78.     <!-- 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。  
  79.          比如AssertionHolder.getAssertion().getPrincipal().getName()。   
  80.          这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息 -->  
  81.     <filter>  
  82.         <filter-name>CAS Assertion Thread Local Filter</filter-name>  
  83.         <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>  
  84.     </filter>  
  85.     <filter-mapping>  
  86.         <filter-name>CAS Assertion Thread Local Filter</filter-name>  
  87.         <url-pattern>/*</url-pattern>  
  88.     </filter-mapping>  
  89.     <!-- ****************** 单点登录结束 ********************-->  

对于这些参数的含义和解释可以参考官方文档也可以看我们之前的第一篇文章:

https://github.com/apereo/java-cas-client

http://blog.csdn.net/zzq900503/article/details/54646828

ps:

域名映射小节已经讲了,这里再强调一次。

如果是没有取消https认证的话casServerLoginUrl和casServerUrlPrefix必须使用域名,且域名要和证书中的“名字与姓氏”完全相同,没有域名的可以配置本地hosts做映射 。我们这里已经去取消了https的协议,让http也能访问所以不需要与证书对应,但是也需要设置一下域名映射。

serverName:是cas-client本项目的ip+port,我们这里cas client项目也放在tomcat 9000中,所以端口也是一样的。

最终的web.xml为:

[java]  view plain  copy

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <web-app version="2.5"   
  3. <span style="white-space:pre">  </span>xmlns="http://java.sun.com/xml/ns/javaee"   
  4. <span style="white-space:pre">  </span>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
  5. <span style="white-space:pre">  </span>xsi:schemaLocation="http://java.sun.com/xml/ns/javaee   
  6. <span style="white-space:pre">  </span>http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">  
  7.   <display-name></display-name><span style="white-space:pre">  </span>  
  8.   <welcome-file-list>  
  9.     <welcome-file>index.jsp</welcome-file>  
  10.   </welcome-file-list>  
  11.     
  12.  <!-- ****************** 单点登录开始 ********************-->  
  13.     <!-- 用于实现单点登出功能  可选 -->  
  14.     <listener>  
  15.         <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>  
  16.     </listener> 
  17.     <!-- 该过滤器用于实现单点登出功能,单点退出配置,一定要放在其他filter之前 可选 -->  
  18.     <filter>  
  19.         <filter-name>CAS Single Sign Out Filter</filter-name>  
  20.         <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>  
  21.         <init-param>  
  22.             <param-name>casServerUrlPrefix</param-name>  
  23.             <param-value>http://casserver:9000/cas/</param-value>  
  24.         </init-param>  
  25.     </filter>  
  26.     <filter-mapping>  
  27.         <filter-name>CAS Single Sign Out Filter</filter-name>  
  28.         <url-pattern>/*</url-pattern>  
  29.     </filter-mapping>  
  30.     <!-- 该过滤器负责用户的认证工作,必须 -->  
  31.     <filter>  
  32.         <filter-name>CASFilter</filter-name>  
  33.         <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>  
  34.         <init-param>  
  35.             <!--casServerLoginUrl:cas服务的登陆url -->  
  36.             <param-name>casServerLoginUrl</param-name>  
  37.             <param-value>http://casserver:9000/cas/login</param-value>  
  38.         </init-param>  
  39.         <init-param>  
  40.             <!--serverName:本项目的ip+port -->  
  41.             <param-name>serverName</param-name>  
  42.             <param-value>http://localhost:9000</param-value>  
  43.         </init-param>  
  44.         <init-param>  
  45.             <param-name>useSession</param-name>  
  46.             <param-value>true</param-value>  
  47.         </init-param>  
  48.         <init-param>  
  49.             <param-name>redirectAfterValidation</param-name>  
  50.             <param-value>true</param-value>  
  51.         </init-param>  
  52.     </filter>  
  53.     <filter-mapping>  
  54.         <filter-name>CASFilter</filter-name>  
  55.         <url-pattern>/*</url-pattern>  
  56.     </filter-mapping>  
  57.     <!-- 该过滤器负责对Ticket的校验工作,必须-->  
  58.     <filter>  
  59.         <filter-name>CAS Validation Filter</filter-name>  
  60.         <filter-class>  
  61.             org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter  
  62.         </filter-class>  
  63.         <init-param>  
  64.             <param-name>casServerUrlPrefix</param-name>  
  65.             <param-value>http://casserver:9000/cas/</param-value>  
  66.         </init-param>  
  67.         <init-param>  
  68.             <param-name>serverName</param-name>  
  69.             <param-value>http://localhost:9000</param-value>  
  70.         </init-param>  
  71.     </filter>  
  72.     <filter-mapping>  
  73.         <filter-name>CAS Validation Filter</filter-name>  
  74.         <!-- 对项目中的哪些路径做登录拦截-->  
  75.         <url-pattern>/*</url-pattern>  
  76.     </filter-mapping>  
  77.     <!-- 该过滤器对HttpServletRequest请求包装, 可通过HttpServletRequest的getRemoteUser()方法获得登录用户的登录名,可选 -->  
  78.     <filter>  
  79.         <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>  
  80.         <filter-class>  
  81.             org.jasig.cas.client.util.HttpServletRequestWrapperFilter  
  82.         </filter-class>  
  83.     </filter>  
  84.     <filter-mapping>  
  85.         <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>  
  86.         <url-pattern>/*</url-pattern>  
  87.     </filter-mapping>  
  88.     <!-- 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。  
  89.          比如AssertionHolder.getAssertion().getPrincipal().getName()。   
  90.          这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息 -->  
  91.     <filter>  
  92.         <filter-name>CAS Assertion Thread Local Filter</filter-name>  
  93.         <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>  
  94.     </filter>  
  95.     <filter-mapping>  
  96.         <filter-name>CAS Assertion Thread Local Filter</filter-name>  
  97.         <url-pattern>/*</url-pattern>  
  98.     </filter-mapping>  
  99.     <!-- ****************** 单点登录结束 ********************-->  
  100. </web-app>  

测试单点登录cas服务

到这里我们的两个client也部署好了,现在可以测试sso是否能够实现了。

我们先分别把cas-client1的index.jsp和cas-client2的index.jsp页面修改一下,使它们能够区分出来。

我这里改的是body的内容,分别改成cas-client1和cas-client2。

 

然后把cas-client1和cas-client2两个项目都加入到tomcat 9000的webapp中 运行tomcat。

 

启动tomcat确认不报错之后我们访问

localhost:9000/cas-client2

发现它成功被拦截了。

跳转到了

http://casserver:9000/cas/login?service=http%3A%2F%2Flocalhost%3A9000%2Fcas-client2%2F

然后我们再访问

localhost:9000/cas-client1

则是被拦截跳转到了

http://casserver:9000/cas/login?service=http%3A%2F%2Flocalhost%3A9000%2Fcas-client1%2F

 

然后我们输入帐号密码。

casuser和Mellon

登录成功后调整到了cas-client1的首页

然后我们再直接访问cas-client2的项目地址

localhost:9000/cas-client2

发现不需要再输入帐号密码,也可以登录了。

 

到这里说明我们的SSO单点登录已经部署验证成功了。

后续我们会进行一些优化和调优等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值