Spring Security学习笔记

这篇博客详细记录了Spring Security 4.1.3.RELEASE的学习过程,包括核心模块、获取方式、各模块用途,以及Java配置的设置,如URL认证、登录页面、权限控制、CSRF防护、session管理和角色授权等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Security学习笔记

这篇文章是在学习Spring Security 4.1.3.RELEASE版本是做的学习笔记,相当于是一边看一边记录吧。以前使用过一段时间Spring Security,但是发现过一段时间之后,以前看的用的东西基本上全部还回去了,这次看这个吧就想着在这上面先把自己看的内容纪录一下吧,为自己后面使用做一个参考吧。

学习地址:http://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/reference/htmlsingle/#what-is-acegi-security

2.4  获取Spring Security

2.4.1  通过在Spring Security官网上面去下载自己需要的jar包,这里就不贴出具体的地址了,上Spring.io就可以找到的。

2.4.2  通过maven方式获取:这里还有一些快照版本以及其他的一些配置,这里不记录了。

<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>4.1.3.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>4.1.3.RELEASE</version>
</dependency>
</dependencies>
2.4.3  通过grandle方式:
dependencies {
	compile 'org.springframework.security:spring-security-web:4.1.3.RELEASE'
	compile 'org.springframework.security:spring-security-config:4.1.3.RELEASE'
}
2.4.4 工程模块的讲解

在Spring Security3.0版本之后开始分离了很多个模块,这样我们可以通过自己的需要来添加需要的jar包,下面来讲解每个分割开的模块的具体用处吧。

  • Core - spring-security-core.jar  这个jar包不多说了,要使用Spring Security就必须引入这个jar包,没商量。这个jar包可以应用在单独的应用中,远程客户端调用,Service调用以及数据库层的调用等等,包含下面的package:

org.springframework.security.core

org.springframework.security.access

org.springframework.security.authentication

org.springframework.security.provisioning

  • Remoting - spring-security-remoting.jar 这个jar包在集成Spring Remote的时候要使用到,或者在有remote client情景的时候需要到这个jar包。
  • Web - spring-security-web.jar 这个jar包在创建web应用的时候需要引入。
  • Config - spring-security-config.jar 这个jar包是一个配置支持包,主要在使用Spring Security的配置(包含XML或者java configuration)时需要。
  • LDAP - spring-security-ldap.jar 在需要使用LDAP认证的时候需要这个jar包。
  • ACL - spring-security-acl.jar  在创建domain object实例的时候需要使用这个jar包。
  • CAS - spring-security-cas.jar 这个jar的意思不是很明白,好像是说是使用单点登录的时候会用到这个jar包。
  • OpenID - spring-security-openid.jar
  • Test - spring-security-test.jar

到这里基本上所有的jar包都介绍完了,个人认为在一般简单的web应用中只需要将core,web,config这几个jar包引入基本上就可以使用简单的Spring Security了。

5  java configuration

这里讲解的主要是通过使用java文件来配置Spring Security。
5.1 最基本的配置。
<span style="font-size:18px;">import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()
				.withUser("user").password("password").roles("USER");
	}
}</span>
值得注意的是:AuthenticationManagerBuilder类与@EnableWebSecurity。
这个类中做了如下事情:
  • 认证应用中所有的url
  • 自动产生一个登陆页面
  • 可以通过“user”与"password"进行登陆
  • 允许登出更能
  • 阻止CSRF攻击
  • session的保护
5.1.1 使用AbstractSecurityWebApplicationInitializer。
这个类的主要作用是保证filters注册springSecurityFilterChain。
5.1.2  使用AbstractSecurityWebApplicationInitializer但是没有使用Spring MVC。
import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

	public SecurityWebApplicationInitializer() {
		super(WebSecurityConfig.class);
	}
}
SecurityWebApplicationInitializer这个类主要完成对springSecurityFilterChain的注册以及使用classLoaderListener加载SecurityWebConfig类。
5.1.3 使用AbstractSecurityWebApplicationInitializer 使用Spring MVC。
如果已经使用了Spring Mvc组件,我们就已经存在一个WebApplicationInitializer来加载配置信息,这个时候我们的SecurityWebApplicationInitializer将会是这个样子:
<span style="font-size:18px;">import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}</span>
这里我们主要完成了对springSecurityFilterChain的注册,但是SecurityWebConfig并没有被加载。这里我们可以在WebApplicationInitializer中来加载SecurityWebConfig类的配置信息:
public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] { WebSecurityConfig.class };
	}

	// ... other overrides ...
}
好了,这里基本上记录了如何通过java 文件来配置Spring Security。
5.3 HttpSecurity
到这里其实对于一个Spring Security的配置是非常不能够用的,只是将用户的登录的认证完成了,并且还没有讲解怎么来自己创建登录页面之类的。
到这里就轮到WebSecurityConfigurerAdapter上场了,这个类中有一个configure(HttpSecurity http)方法,其具体实现如下:
protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.and()
		.httpBasic();
}
从这个方法的实现了如下功能:
  • 应用中所有的url都必须user进行认证
  • user只有通过登录进行认证
  • 允许用户通过基本的http认证
当然也可以通过XML文件来完成上面的配置:
<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

5.4 使用自定义的登录页面
protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.loginPage("/login") 1
			.permitAll();        2
}
标识‘1’表示使用自定义的登录页面;标识‘2’表示允许所有用户都可以访问登陆页面。
一个示例的登录页面如下:
<span style="color: rgb(111, 111, 111);"><c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">       1
	<c:if test="${param.error != null}">        2
		<p>
			Invalid username and password.
		</p>
	</c:if>
	<c:if test="${param.logout != null}">       3
		<p>
			You have been logged out.
		</p>
	</c:if>
	<p>
		<label for="username">Username</label>
		<input type="text" id="username" name="username"/>	4
	</p>
	<p>
		<label for="password">Password</label>
		<input type="password" id="password" name="password"/>	5
	</p>
	<input type="hidden"                        6
		name="${_csrf.parameterName}"
		value="${_csrf.token}"/>
	<button type="submit" class="btn">Log in<</span><span style="color:#330033;">/</span><span style="color:#6f6f6f;">button>
</form></span>
  • 标识‘1’:登录成功即将跳转的页面
  • 标识‘2’:没有通过登录认证时的错误信息提示
  • 表示‘3’:登出功能提示信息
  • 标识‘4’:登录username
  • 标识‘5’:登录password
  • 标识‘6’:crsf相关配置信息,后续讲解吧。
5.4  配置多样化的url拦截
protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()                                                                1
			.antMatchers("/resources/**", "/signup", "/about").permitAll()                  2
			.antMatchers("/admin/**").hasRole("ADMIN")                                      3
			.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            4
			.anyRequest().authenticated()                                                   5
			.and()
		// ...
		.formLogin();
}
标识‘2’表示所有通过“/resources/**”,"/signup","/about"开始的request url将通过验证;
标识‘3’表示只有"ADMIN”权限的用户才能访问“/admin/**”开始的request;
标识‘4’表示只有“DBA”与“ADMIN”权限的用户才能访问“/db/**”开始的request;
标识‘5’表示只要用户通过登录验证就可以访问这些url。

5.5  处理登出功能
WebSecurityConfigurerAdapter为我们自动提供了登出功能,在Spring Security中默认的登出url是“应用地址+/logout”,当进行登出功能的时候,将完成如下功能:
  • 清除Http Session中的内容
  • 清除RememberMe
  • clearing the SecurityContextHolder
  • 跳转到/login?logout
接下来记录一下一个比较具体的java config的配置吧:
protected void configure(HttpSecurity http) throws Exception {
	http
		.logout()                                                                1
			.logoutUrl("/my/logout")                                                 2
			.logoutSuccessUrl("/my/index")                                           3
			.logoutSuccessHandler(logoutSuccessHandler)                              4
			.invalidateHttpSession(true)                                             5
			.addLogoutHandler(logoutHandler)                                         6
			.deleteCookies(cookieNamesToClear)                                       7
			.and()
		...
}
标识‘2’表示进行登出的时候的url地址;
标识‘3’表示登出成功之后将要跳转到的地址,一般是跳转到对应的登录页面上去;
标识‘4’表示登出成功之后自定义实现将要进行的处理,使用这个功能将屏蔽标识‘3’使用的logoutSuccessUrl的功能;
标识‘5’表示是否清楚Http session中的内容;
标识‘6’表示添加一个LogoutHandler,SecurityContextLogoutHandler将添加到LogoutHandler最后;
标识‘7’表示清除指定的名字的cookie。

5.5.1  LogoutHandler
这个接口用来完成对logout功能的处理,主要有一些清除cookie、session、remenberme内容;实现这个接口的类有:
PersistentTokenBasedRememberMeServices
TokenBasedRememberMeServices
CookieClearingLogoutHandler
CsrfLogoutHandler
SecurityContextLogoutHandler
这些实现了这个LogoutHandler接口的类是一些快速处理对应内容的handler,比如清理cookie的时候可以使用CookieClearingLogoutHandler。

5.5.2 LogoutSuccessHandler
这个在登出成功之后LogoutFilter调用这个LogoutSuccessHandler,比如指定跳转到的地址。

5.6.2 JDBC认证
<span style="color:#6d180b;">@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.jdbcAuthentication()
			</span><span style="color:#ff0000;">.dataSource(dataSource)</span><span style="color:#6d180b;">
			.withDefaultSchema()
			.withUser("user").password("password").roles("USER").and()
			.withUser("admin").password("password").roles("USER", "ADMIN");
}</span>
5.6.4 AuthenticationProvider
我们可以通过实现AuthenticationProvider来完成自定义的authentication。但是这个只有在AuthenticationManagerBuilder没有被使用到的时候
才会有效 (this is only used if the AuthenticationManagerBuilder has not been populated).
@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
	return new SpringAuthenticationProvider();
}

5.6.5 UserDetailsService
我们也可以通过实现UserDetailsService来完成自定义的authentication。但是这个只有在没有配置AuthenticationManagerBuilder
以及AuthenticationProvider的时候才有有效。
@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
	return new SpringDataUserDetailsService();
}
当然我们也可以自定义password的编码方式,通过实现PassWordEncoder。例如:
@Bean
public BCryptPasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}


5.7 多样化的HttpSecurity
在XML配置文件中,我们仅仅通过<http>标签就能完成多样化的配置;这种配置的关键是要继承WebSecurityConfigurationAdapter
下面这是java文件多样化的配置:
@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) { 1
		auth
			.inMemoryAuthentication()
				.withUser("user").password("password").roles("USER").and()
				.withUser("admin").password("password").roles("USER", "ADMIN");
	}

	@Configuration
	@Order(1)                                                        2
	public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/api/**")                               3
				.authorizeRequests()
					.anyRequest().hasRole("ADMIN")
					.and()
				.httpBasic();
		}
	}

	@Configuration                                                   4
	public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.anyRequest().authenticated()
					.and()
				.formLogin();
		}
	}
}
标识'1'表示配置一个全局的认证;
标识'2'表示在用户有Amin权限的时候才能访问'/api'路径下资源的访问,@Order表示这个认证的优先级别;
标识‘4’表示用户在访问非'/api'路径的时候的认证方式,没有使用@Order将在最后执行这个认证;

5.8 方法上的security
From version 2.0 onwards Spring Security has improved support substantially for adding security 
to your service layer methods. It provides support for JSR-250 annotation security as well as the 
framework’s original @Secured annotation. From 3.0 you can also make use of new expression-based 
annotations. You can apply security to a single bean, using the intercept-methods element to decorate 
the bean declaration, or you can secure multiple beans across the entire service layer using the AspectJ 
style pointcuts.

5.8.1 EnableGlobalMethodSecurity
在@Configuration配置上加上一个@EnableGlobalMethodSecurity注解,这样我们就可以在方法上面使用@Secured注解了,具体
实现如下:
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}
然后在service层我们就可以做如下操作:
public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
这样在使用具体的方法的时候就必须先通过认证。也就是对于post(Account account,double amount)方法只有在拥有'ROLE_TELLER'
权限的用户才能访问。

使用JSR-250
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}
另外我们还可以通过如下进行方法的配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
具体的使用可以通过:
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

5.8.2 GlobalMethodSecurityConfiguration
我们可以通过实现GlobalMethodSecurityConfiguration来完成自定义的方法的配置,比如通过:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
	@Override
	protected MethodSecurityExpressionHandler createExpressionHandler() {
		// ... create and return custom MethodSecurityExpressionHandler ...
		return expressionHandler;
	}
}


6 Security NameSpace Configuration

6.1 Introduction
这里主要讲解如何通过xml文件来配置SprignSecurity,首先在使用的时候我们需要加入spring-security-config的jar
包到我们的项目中,然后:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans>
这个是要使用到Spring Security的时候我们需要将对应的shcema添加到xml文件中。
但是如果在我们项目中配置文件分离过多的话,我们也可以进行如下配置,方便我们阅读:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans:beans>

6.2 如何配置Spring Security中的xml文件
6.2.1 web.xml文件中的配置:在这之中加入springSecurityFilterChain的filter
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

6.2.2 基本的Spring security的配置:
<http>
<intercept-url pattern="/**" access="hasRole('USER')" />
<form-login />
<logout />
</http>
在测试项目中,我们只需要在Spring Security的配置文件中加入这几行代码,便可以来进行简单的使用了。
这几行配置包含如下信息:
  • 所有的Url只有拥有‘USER’角色的用户才能访问到
  • 只能通过'username','password'进行登录
  • 拥有登出功能

接下来我们可以通过如下配置来创建几个测试用户:
<authentication-manager>
<authentication-provider>
	<user-service>
	<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="bobspassword" authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>
在这里<authentication-provider>将会为我们创建DaoAuthenticationProvider的实例,<user-service>将创建出
InMemoryDaoImpl的实例,<authentication-manager>会创建ProviderManager实例并为我们注册所有的 authentication
providers。

6.2.3 创建我们自己的登录页面
到这里我们基本都提到登录,但是并没有写出一个登录的页面出来,我们的测试也还是可以运行,这是因为Spring Security会
自动为我们创建一个基本的登录页面出来。
不过在实际使用中我们还是需要自己的登录页面的,那么在Spring Security中如何来进行配置呢?
<span style="font-size:18px;"><http>
<intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" /> 2
<form-login login-page='/login.jsp'/>
</http></span>
就像这样,<form-login>中为我们指定了一个login.jsp页面,这个就将使用到我们自己的登录页面了。
但是上面这个示例中存在一个问题,标识‘2’处说的是所有的只有拥有‘ROLE_USER’的用户才能访问到,这当然也包含
login.jsp,这样显然是不合理的,为了解决上面这个问题,我们还需要改动我们的配置文件为:
<span style="font-size:18px;"><http pattern="/css/**" security="none"/>
<http pattern="/login.jsp*" security="none"/>

<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page='/login.jsp'/>
</http></span>
通过这个配置,我们的login.jsp文件将不再受Security的验证,保证所有的用户都能够访问我们的登录页面。

6.2.5 创建authentication providers
前面我们使用Spring Security时用户信息只能随意写那么一两个来测试,但是对于实际项目中将用户信息保存在数据库这种方式怎么办呢?下面我们来看看在Spring Security中怎么配置:
<span style="font-size:18px;"><authentication-manager>
	<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager></span>
这中的myUserDetailsService是MyUserDetailsService注入在Spring中的bean的名字,并且MyUserDetialsService这个类实现了UserDetailsService这个接口。接下来用户的信息我们只要通过MyUserDetailsService这个类来获取便可。
另外:我们还可以直接通过dataSource来配置athentication provider,如下:
<authentication-manager>
<authentication-provider>
	<jdbc-user-service data-source-ref="securityDataSource"/>
</authentication-provider>
</authentication-manager>
这中的securityDataSource是applicationContext中关于DataSource配置的bean的名字,当然这里面要有我们需要的存储用户信息的表。
另外:我们也可以使用JdbcDaoImpl这个类来完成获取用户信息,如下
<authentication-manager>
<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>

<beans:bean id="myUserDetailsService"
	class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
当然,我们可以自定义自己需要的Authentication Provider来完成自己的需求,具体配置如下
<authentication-manager>
	<authentication-provider ref='myAuthenticationProvider'/>
</authentication-manager>
这里的myAuthenticationProvider表示一个实现了AuthenticationProvider的类的bean的名称。

Adding password encoder。
<beans:bean name="bcryptEncoder"
	class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<authentication-manager>
<authentication-provider>
	<password-encoder ref="bcryptEncoder"/>
	<user-service>
	<user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f"
			authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f"
			authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>


6.3.2 选择HTTp或者https
<http>
<intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
<intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
...
</http>
另外,我们还可以配置自己需要的端口,
<http>
...
<port-mappings>
	<port-mapping http="9080" https="9443"/>
</port-mappings>
</http>

6.3.3 session的管理
在项目中如果session无效的话,我们可以通过如下配置来将已经登录的用户跳转到指定的页面(一般是登录页面)
<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>
这个时候我们还需要删除掉客户端的cookie,如下:
<http>
<logout delete-cookies="JSESSIONID" />
</http>

当前session控制,如果项目中需要设置当前用户在当前浏览器中不能重复登录的情况下,我们首先可以在web.xml文件中添加
<listener>
<listener-class>
	org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
然后再Spring Security配置文件中添加
<http>
...
<session-management>
	<concurrency-control max-sessions="1" />
</session-management>
</http>
这样用户在进行第二次登录的时候将会被阻止,当然我们也可以配置一下让用户在第二次登录的时候跳转到指定的page页面或给予提示信息
<http>
...
<session-management>
	<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
这样,如果用户在已经登录一次之后将会使 authentication-failure-url 指定的url 。或者我们可以在session-management元素中指定session-authentication-error-url
属性来指定登录失败的page。

我们可以在<session-management>中配置session-fixation-protection解决web应用程序中session固定攻击问题,这个配置有如下
可选项:
  • none  什么都不做
  • newSession 每次登陆都创建一个新的session,不会拷贝一个已有的session的信息
  • migrateSession 每次登录都创建一个新的session,但是会拷贝以前session中的信息到新session中,这是Spring Security默认配置
  • changeSessionId 不会创建新的session,使用servlet中提供的session的保护方式,这个只有在servlet3.1之后才有,之前的版本会抛出异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值